学海无涯

Rancher 高可用部署

0x00

其实官网已经有了无坑且完备的高可用部署方案 官方部署方案链接,但是太过翔实,这里只是记录一下自己的部署方案

说明

本教程是基于k3s安装Rancher Server,从Rancher V2.4开始支持在K3s集群安装,K3s比RKE更新,易于使用且更轻量,全部组件都打包在了一个二进制文件里。

前置条件

mysql已安装,配置账户及访问权限

创建可读写rancher database的账户,限定可访问ip为rancher server所在服务器ip

create user rancher identified by 'rancher#1Yer';
grant all privileges on rancher.* to rancher@'<yourNodeIP1>' identified by 'rancher#1Yer';
grant all privileges on rancher.* to rancher@'<yourNodeIP2>' identified by 'rancher#1Yer';
flush privileges;

安装docker


# 安装依赖yum install -y yum-utils device-mapper-persistent-data lvm2
# 添加源
yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
 
# 更新yum缓存
yum clean all
yum makecache fastyum -y
 
# 安装docker ce
yum install docker-ce-19.03.5-3.el7.x86_64
 
# 通过systemctl启动服务
systemctl start docker
  
# 开机自启动
systemctl enable docker
 
# 关闭docker 将docker存储位置转为/data目录(防止系统盘被占满),data目录根据实际情况修改
systemctl stop docker
 
mv /var/lib/docker /data/docker
ln -s /data/docker /var/lib/docker

安装kubectl

wget https://storage.googleapis.com/kubernetes-release/release/v1.18.2/bin/linux/amd64/kubectl
 
chmod +x ./kubectl
 
sudo ln -s ./kubectl /usr/local/bin/kubectl
 
kubectl version --client

安装Helm


# 下载安装helm
wget https://get.helm.sh/helm-v3.2.0-linux-amd64.tar.gz
tar -zxvf helm-v3.2.0-linux-amd64.tar.gz
mv linux-amd64/helm /usr/local/bin/helm
 
# 添加 Helm Chart 仓库
helm repo add rancher-stable> https://releases.rancher.com/server-charts/stable

部署 k3s 集群

在待部署的机器上分别执行

Helm 编排教程

Helm简介

我们知道 Kubernetes 是一个分布式的容器集群管理系统,它把集群中的管理资源抽象化成一个个 API 对象,并且推荐使用声明式的方式创建,修改,删除这些对象,每个 API 对象都通过一个 yaml 格式或者 json 格式的文本来声明。这带来的一个问题就是这些 API 对象声明文本的管理成本,每当我需要创建一个应用,都需要去编写一堆这样的声明文件。

Helm 就是用来管理这些 API 对象的工具。它类似于 CentOS 的 YUM 包管理,Ubuntu 的 APT 包管理,你可以把它理解成 Kubernetes 的包管理工具。它能够把创建一个应用所需的所有 Kubernetes API 对象声明文件组合并打包在一起。并提供了仓库的机制便于分发共享,还支持模版变量替换,,同时还有版本的概念,使之能够对一个应用进行版本的管理。

Helm 采用客户端/服务器架构,有如下组件(概念)组成:

  • Chart: 就是 Helm 的一个包(package),包含一个应用所有的 Kubernetes manifest 模版,类似于 YUM 的 RPM 或者 APT 的 dpkg 文件。
  • Helm CLI: Helm 的客户端组件,它通过 gRPC aAPI 向 tiller 发送请求。
  • Tiller: Helm 的服务器端组件,在 Kubernetes 群集上运行,负载解析客户端端发送过来的 Chart,并根据 Chart 中的定义在 Kubernetes 中创建出相应的资源,tiller 把 release 相关的信息存入 Kubernetes 的 ConfigMap 中。
  • Repository: 用于发布和存储 Chart 的仓库。
  • Release: 可以理解成 Chart 部署的一个实例。通过 Chart 在 Kubernetes 中部署的应用都会产生一个唯一的 Release,即使是同一个 Chart,部署多次就会产生多个 Release。

安装 Helm

Helm CLI 端的安装

  1. 直接下在 Helm CLI 的二进制 release
  2. 解压并移动至 PATH tar -zxvf helm-v2.0.0-linux-amd64.tgz mv linux-amd64/helm /usr/local/bin

Tiller 的安装

为 Tiller 创建 K8S 的 RBAC 角色


apiVersion: v1
kind: ServiceAccount
metadata:
  name: tiller
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: tiller
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
  - kind: ServiceAccount
    name: tiller
    namespace: kube-system

安装 Tiller, 默认使用的是 ~/.kube/config 中的 CurrentContext 来指定部署的 k8s 集群,默认安装在 namespace 为 kube-system 下,init 时可以指定很多可选参数,更多请参考官方文档

Celery 4 初体验及踩坑

Celery是基于分布式消息传递的开源异步任务队列或作业队列。虽然它支持调度,但其重点是实时操作。现在4版本已经步入稳定,而国内互联网的几乎都是3版本的教程。所以这里记录下4版本下的踩坑及外文解决方案的翻译记录。

win环境运行celery 4 worker

Celery 是一个资金最少的项目,因此我们不支持 Microsoft Windows。请不要提出与该平台相关的任何问题。

官方在4版本移除了win平台支持,但是经过查阅,只要使用将并发模式-P改为gevent或者eventlet即可正常启动,但并不知道会有什么影响,毕竟官方已经不提供支持了,该启动方法仅适用于本地调试。

附上worker启动脚本

# celery_worker_start.bat

@echo off

chcp 65001

CLS

echo 正在启动 python 虚拟环境

CALL venv\Scripts\activate.bat

echo 正在启动 celery

celery -A multi_analysis_tasks.celery_app worker -P gevent -l info

flask + celery 启动蓝图循环引用问题

项目结构

def register_plugin(application):
    from app.models.base import db
    db.init_app(application)
    with application.app_context():
        db.create_all()


def create_app():
    application = Flask(__name__)
    CORS(application, supports_credentials=True)  # 设置允许跨域
    application.config.from_object('app.config.setting')
    application.config.from_object("app.config.secure")
    register_blueprint(application)
    register_plugin(application)
    return application

app = create_app()
celery_app = make_celery(app)

if __name__ == "__main__":

    app.run(host="0.0.0.0", port=5000, debug=False)

>>> ImportError: cannot import name 'create_blueprint_v1'

解决方案

celery worker 入口文件和 flask 启动的入口文件分开,worker 的入口文件不需要初始化蓝图,即可解决循环引用的问题。

LRU实现

缓存及在 Python 中使用缓存

本文大致上是基于 caching-in-python 这篇文章的翻译

缓存操作

缓存操作主要有两种类型。缓存如浏览器缓存,服务器缓存,代理缓存,硬件缓存工作原理的读写缓存。当处理缓存时,我们总是有大量的内存需要花费大量的时间来读写数据库、硬盘。 缓存则能帮我们加快这些任务。

读缓存

每次客户端向存储请求数据时,请求都会先去访问与存储相关联的缓存。

  1. 如果请求的数据在缓存上可用,那么他就是一个Cache hit

Cache hit

  1. 如果没有命中缓存。就是Cache miss,则需要去DB中取数据。

Cache miss

  1. 当请求缓存的时刻,其他一些进程改变了DB中的数据,从而更新了缓存。导致当前请求的缓存过期,这是一个Cache invalidation,也叫脏数据。

Cache invalidation

写缓存

顾名思义,写缓存支持快速写操作。设想一个写入量很大的系统,我们都知道向 DB 写入代价很高。而缓存很方便,可以处理 DB 写加载,这些加载稍后会批量更新到 DB。需要注意的是,DB 和缓存之间的数据应该始终保持同步。有3种方法可以实现写缓存。

  1. Write Through
  2. Write Back
  3. Write Around

写缓存

Write Through

客户端先写缓存,缓存再写到 DB ,DB 返回结果给客户端。对缓存要求强一致性可以用这种方式。

  • 优点: 缓存和存储之间不会存在数据不匹配,数据一致
  • 缺点: 缓存和存储都需要更新,这会产生额外的开销。

Write Back

客户端先写到 DB ,DB 直接返回结果给客户端。之后 DB 定时将数据同步到缓存,下一次客户端读数据时先请求缓存。

  • 优点: 加快写缓存的速度
  • 缺点: 无法保证数据一致性

Write Around

客户端直接将数据写入 DB,只有在读数据的时候,才从 DB 中加载数据到缓存。

  • 优点

    • 写入后未立刻读取的数据不会重载缓存
    • 减少写方法的延迟
  • 缺点

    • 读取最近写入的数据将导致缓存丢失,并且不适合这种用例

缓存回收策略

缓存使读写速度更快。那么,只有从缓存中读取和写入所有数据才有意义,而不是使用 DB。但是,只是因为缓存很小所以速度快。缓存越大,搜索时间越长。

ssdb、minio性能测试

项目上需要找一个硬盘型的NoSQL,用于将Redis中的冷数据落入硬盘。初步选型了几款key-value类型的NoSQL,分别有levelDB、 rocksDB、 TiDB、 SSDB、swapDB。均为基于levelDB开发的几款NoSQL。其中因为levelDB、rocksDB无网络接口,不方便做分布式和高可用。,TiDB过重,还有swapDB社区不够活跃且相关client API不完备。暂时选型SSDB

项目需要存储的其实是一个略长的二进制字符串,初步认为,使用对象存储方案其实也可以替代NoSQL,所以压测对象添加当前非常火的云原生对象存储MinIO

压测配置

硬件名|配置 系统| Ubuntu(基于win10 wsl版的docker启动) 内存| 16GB(实际可用6.08G) CPU| Intel i5-8400

读写测试

测试项目: 1. 写50M数据100次 2. 随机读取任意key 100次(对LRU机制不友好)

SSDB

数据导入成功!
数据序列化成功!
a 数据大小:50.99295234680176 MB
第1次写入总用时: 797 ms
第2次写入总用时: 848 ms
第3次写入总用时: 3621 ms
第4次写入总用时: 813 ms
第5次写入总用时: 1862 ms
第6次写入总用时: 838 ms
第7次写入总用时: 2235 ms
第8次写入总用时: 836 ms
第9次写入总用时: 900 ms
第10次写入总用时: 1027 ms
第11次写入总用时: 1101 ms
第12次写入总用时: 880 ms
第13次写入总用时: 1956 ms
第14次写入总用时: 866 ms
第15次写入总用时: 2422 ms
第16次写入总用时: 852 ms
第17次写入总用时: 4511 ms
第18次写入总用时: 875 ms
第19次写入总用时: 2736 ms
第20次写入总用时: 814 ms
第21次写入总用时: 7172 ms
第22次写入总用时: 891 ms
第23次写入总用时: 7820 ms
第24次写入总用时: 836 ms
第25次写入总用时: 22103 ms
第26次写入总用时: 877 ms
第27次写入总用时: 2712 ms
第28次写入总用时: 841 ms
第29次写入总用时: 1928 ms
第30次写入总用时: 916 ms
第31次写入总用时: 839 ms
第32次写入总用时: 826 ms
第33次写入总用时: 7759 ms
第34次写入总用时: 843 ms
第35次写入总用时: 10670 ms
第36次写入总用时: 843 ms
第37次写入总用时: 9361 ms
第38次写入总用时: 821 ms
第39次写入总用时: 810 ms
第40次写入总用时: 794 ms
第41次写入总用时: 13281 ms
第42次写入总用时: 833 ms
第43次写入总用时: 811 ms
第44次写入总用时: 798 ms
第45次写入总用时: 18843 ms
第46次写入总用时: 911 ms
第47次写入总用时: 9428 ms
第48次写入总用时: 898 ms
第49次写入总用时: 17582 ms
第50次写入总用时: 903 ms
第51次写入总用时: 831 ms
第52次写入总用时: 800 ms
第53次写入总用时: 14602 ms
第54次写入总用时: 827 ms
第55次写入总用时: 5898 ms
第56次写入总用时: 856 ms
第57次写入总用时: 5693 ms
第58次写入总用时: 1050 ms
第59次写入总用时: 882 ms
第60次写入总用时: 1020 ms
第61次写入总用时: 15060 ms
第62次写入总用时: 902 ms
第63次写入总用时: 1062 ms
第64次写入总用时: 915 ms
第65次写入总用时: 7572 ms
第66次写入总用时: 823 ms
第67次写入总用时: 9649 ms
第68次写入总用时: 832 ms
第69次写入总用时: 10403 ms
第70次写入总用时: 907 ms
第71次写入总用时: 978 ms
第72次写入总用时: 789 ms
第73次写入总用时: 2111 ms
第74次写入总用时: 947 ms
第75次写入总用时: 4675 ms
第76次写入总用时: 944 ms
第77次写入总用时: 8592 ms
第78次写入总用时: 832 ms
第79次写入总用时: 2940 ms
第80次写入总用时: 842 ms
第81次写入总用时: 19835 ms
第82次写入总用时: 862 ms
第83次写入总用时: 7646 ms
第84次写入总用时: 873 ms
第85次写入总用时: 1002 ms
第86次写入总用时: 842 ms
第87次写入总用时: 9057 ms
第88次写入总用时: 801 ms
第89次写入总用时: 5117 ms
第90次写入总用时: 918 ms
第91次写入总用时: 798 ms
第92次写入总用时: 853 ms
第93次写入总用时: 7728 ms
第94次写入总用时: 810 ms
第95次写入总用时: 3969 ms
第96次写入总用时: 814 ms
第97次写入总用时: 2050 ms
第98次写入总用时: 819 ms
第99次写入总用时: 9566 ms
第100次写入总用时: 833 ms

随机读

k8s中服务添加hosts及一键转换脚本

项目管理k8s集群用的是rancher,可是rancher没有提供给deployment批量添加hosts的图形化界面,所以还是只能按照k8s官方的方法修改yaml文件。

示例

使用 HostAliases 向 Pod /etc/hosts 文件添加条目

apiVersion: v1
kind: Pod
metadata:
  name: hostaliases-pod
spec:
  restartPolicy: Never
  hostAliases:
  - ip: "127.0.0.1"
    hostnames:
    - "foo.local"
    - "bar.local"
  - ip: "10.1.2.3"
    hostnames:
    - "foo.remote"
    - "bar.remote"
  containers:
  - name: cat-hosts
    image: busybox
    command:
    - cat
    args:
    - "/etc/hosts"

一键转换脚本

本脚本仍需要一定的手动操作

  • 将需要修改的hosts文件改为csv格式(记得设置表头)

  • 执行脚本后需要去掉多余的'

import pandas as pd
import yaml

data = pd.read_csv(r"hosts.csv")

ip = data.loc[:, "ip"].tolist()
hosts = data.loc[:, "hosts"].tolist()

res = {
    "hostAliases": []
}

for i in range(len(ip)):
    res_side = {
        "ip": f"\"{ip[i]}\"",
        "hostnames": [f"\"{hosts[i]}\""]
    }
    res['hostAliases'].append(res_side)

with open('hosts.yaml', 'w') as f:
    yaml.dump(res, f)
    f.close()
Kubeconfig

Rancher2 & K8S部署踩坑记录

本安装教程基于CetnOS 7环境编写

安装docker

首先安装依赖并添加国内源

$ sudo yum install -y yum-utils device-mapper-persistent-data lvm2
$ yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

安装docker

$ yum clean all

$ yum makecache fastyum -y

$ yum install docker-ce

启动服务并开机自起

# 通过systemctl启动服务
$ systemctl start docker

# 开机自启动
$ systemctl enable docker

# 验证docker
$ docker version

更改docker 存储目录至空间大的磁盘(默认挂载点为/var/lib/docker)

# 停止docker服务
$ systemctl stop docker

# 迁移目录至空间大的挂载点
$ mv /var/lib/docker /data/docker

# 添加软链接
$ ln -s /data/docker /var/lib/docker

# 重启docker
$ systemctl start docker

安装Rancher2 & K8S集群

安装Rancher2

建议rancher server单独部署在一台机器上,不并入集群内

MySQL连接驱动性能对比

uwsgi 多进程导致数据库连接丢失的踩坑记录

起因

项目使用的 Flask+SQLAlchemy+uwsgi ,突然有一天编写了一个有对数据库高并发的接口。然后其他本来正常的接口就偶尔会出现404错误,且必须重启服务才能解决。

试验①

以为是MySQL连接池和超时时间导致的,反复查看发现并没有什么问题。然后怀疑到是不是python对MySQL的连接驱动导致的。

项目里使用的pymysql被公认为是比较慢的连接驱动。索性换成了mysqldb。 MySQL连接驱动性能对比

结果只是使触发这种bug的频率稍微降低了一点

试验②

后来就怀疑到是不是uwsgi起多进程的时候触发了什么奇怪的bug,结果一搜就在Stack Overflow上发现了宝藏。

When working with multiple processes with a master process, uwsgi initializes the application in the master process and then copies the application over to each worker process. The problem is if you open a database connection when initializing your application, you then have multiple processes sharing the same connection, which causes the error above.

简单翻译一下,就是uwsgi启动多进程时,会启动一个主进程初始化所有的app(其中包括数据库连接),然后将所有app复制到其他进程中。这!就!导!致!了!所有进程全部共用一个MySQL的连接

如果在uwsgi.ini中添加参数lazy-apps=true,即可让各个进程都创建自己的app。即所有进程都有属于自己的MySQL连接了。

详细地址: using-flask-sqlalchemy-in-multiple-uwsgi-processes

构建Python Dockerfile的奇淫巧技

镜像构建

前言

最简单的情况下,如果我们使用官方python镜像,构建我们的容器会无敌庞大。因为他帮我们预置了许许多多类库。同时我们直接使用RUN pip install /xxx/requirements.txt安装环境时,每次构建镜像都会从pip仓库里面拉包,也会非常慢。

FROM python:3.7
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
CMD ["uwsgi", "--ini", "/xxx/uwsgi.ini"]

构建requirements缓存(使用空间换时间)

FROM python:3.7
COPY requirements.txt /
RUN pip install -r /requirements.txt
COPY src/ /app
WORKDIR /app
CMD ["uwsgi", "--ini", "/xxx/uwsgi.ini"]

用这种方式重写我们的 Dockerfile,可以利用 Docker 的层缓存,如果 requirements.txt 文件不变,则跳过安装 pip 包。

使用alpine镜像(使用时间换空间)

FROM python:3.6-alpine

COPY requirements.txt /

RUN pip install --upgrade pip -i https://pypi.douban.com/simple \
    && pip install -r /requirements.txt -i https://pypi.douban.com/simple

RUN mkdir /mailAlarm

WORKDIR /mailAlarm

COPY . /mailAlarm

VOLUME /mailAlarm/config

RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories \
    && apk add --no-cache tzdata \
    && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
    && echo 'Asia/Shanghai' >/etc/timezone

CMD ["python", "-u", "/mailAlarm/run.py"]

这种方式构建镜像就能得到很小的镜像,但是需要额外安装部分pip包所依赖的类库。因为alpine版本镜像默认是只安装python环境所需要的基础类库。

Authlib 单点登录库初体验及踩坑

起因

项目突然要接入TX云,理所应当的要使用tx的单点登录了。于是乎,经过各方推荐,使用了大名鼎鼎的Authlib库。

初体验

经过各方文档,整理了一下,在Flask中使用Authlib相当简单。如果是接入有名的OAuth2站点如GithubGoogle这种,直接使用官方已经封装好的类即可快速实现,但此处使用的是TX方为工业互联网平台新搭建的OAuth2服务,理所应当不能直接使用。但仍可以使用较为便捷的封装进Flask中的认证方法,具体步骤如下:

新建存储Token的表

根据存储的access_token校验后续接口用户登录情况。 其中refresh_tokenaccess_token过期后,下次去OAuth2服务器请求新的Token的字段。如果在注册Authlib对象时写了update方法,即可自动更新token。

from app.models.base import db, Base


class OAuth2Token(db.Model):
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(40))
    token_type = db.Column(db.String(40))
    access_token = db.Column(db.String(200))
    refresh_token = db.Column(db.String(200))
    expires_in = db.Column(db.Integer())
    user_id = db.Column(db.Integer(), db.ForeignKey("user.id"))
    user = db.relationship("User", backref=db.backref("auth_token", lazy="dynamic"))

    def to_token(self):
        return dict(
            access_token=self.access_token,
            token_type=self.token_type,
            refresh_token=self.refresh_token,
            expires_in=self.expires_in,
        )

    @staticmethod
    def save_token(response):
        with db.auto_commit():
            oauth = OAuth2Token()
            oauth.name = 'tencent'
            oauth.access_token = response['access_token']
            oauth.token_type = response['token_type']
            oauth.expires_in = response['expires_in'] if 'expires_in' in response else None
            oauth.refresh_token = response['refresh_token'] if 'refresh_token' in response else None
            oauth.user_id = response['user_id'] if 'user_id' in response else None
            db.session.add(oauth)

创建oauth对象

authlib会自动从flask的全局configapp/config/setting.py中读取你设置的{YOUR_NAME}_CLIENT_ID{YOUR_NAME}__CLIENT_SECRET