Shell-operator:用于简化Kubernetes operator的创建

dummy 发表了文章 • 0 个评论 • 137 次浏览 • 2019-06-18 17:24 • 来自相关话题

我们很高兴在此介绍我们新的开源方案,它能将Kubernetes中Operator的开发提升到一个全新的更简单的水平。它可以让你在15分钟内将你的小脚本变成完全成熟的Operator而吸引你。欢迎 ...查看全部

我们很高兴在此介绍我们新的开源方案,它能将Kubernetes中Operator的开发提升到一个全新的更简单的水平。它可以让你在15分钟内将你的小脚本变成完全成熟的Operator而吸引你。欢迎shell-operator




目标


shell-operator的思路很简单,它订阅来自Kubernetes对象的事件,并在事件发生后执行外部程序,为其提供有关事件的信息:




在我们运行Kubernetes集群期间,很多小任务开始显现。在Flant,我们迫切想要以正确的方式来自动化它们,因此我们觉得需要更智能的解决方案。通常你可以使用基本的bash脚本来解决所有这些任务,但是如你所知,更推荐的方式是使用Golang来编写Operators。很显然,为每个小任务分别开发成熟的Operator将会很低效。


15分钟内创建一个Operator


我们将举一个在Kubernetes集群中可以被自动化的例子,以及shell-operator可以如何帮助我们。我们将尝试复制用于访问Docker仓库的凭证。


使用私有仓库镜像的Pod应在其清单中包含指定的用于访问仓库的secret。这个secret必须在创建Pod之前先创建在每个命名空间中。你可以手动执行此操作,但是,如果我们将配置动态的多个环境,我们将为单个应用程序创建许多命名空间。在多个应用程序(甚至两个或三个)的情况下,secret的数量会变得巨大。关于secret还有一个需求:我们希望能够偶尔更改(注册表)仓库的访问密钥。因此,手动解决方案变得非常低效,你必须自动创建和更新secret。


简单的自动化


我们来写一个脚本,每N秒运行一次,并检查命名空间中secret是否存在。如果secret不存在,那么它将会被创建。这个解决方案的优势是它看起来就像是cron中的一个shell脚本,一种经典且易于理解的方法。缺点是在此脚本的两次启动之间的间隔期间可能会出现一些新的命名空间,因此在一段时间内它将不会持有这个secret。这种情况会导致启动Pod的过程中出错。


使用shell-operator进行自动化


为了使我们的脚本准确运行,经典的cron执行应该被当有新增命名空间事件发生时的执行所取代。在这种情况下,你可以在使用之前创建一个secret。让我们看看如何使用shell-operator来实现这个功能。


首先,我们先分析一下脚本,就shell-operator而言,脚本都被称之为“钩子“。每个钩子在使用--config标志执行时都会通知shell-operator将其绑定(即需要执行哪些事件)。在我们的例子中,我们将使用onKubernetesEvent


#!/bin/bash
if [[ $1 == "--config" ]] ; then
cat <

在这里,我们定义我们关注的namespace类型的添加(add)对象事件。


现在我们需要添加当事件发生时需要执行的代码:


#!/bin/bash
if [[ $1 == "--config" ]] ; then
# configuration
cat <

真棒!我们现在已有一个简洁且漂亮的脚本,想让它能真正发挥作用,我们需要准备一个镜像并将其跑在集群中。


使用钩子制作我们的镜像


你可以很轻易观察到我们在脚本里面使用了kubectljq命令。这意味着镜像中需要包含钩子,shell-operator二进制文件(它将监视事件并执行这个钩子),以及钩子需要用到的命令(kubectljq)。hub.docker.com上已提供了包含shell-operator,kubectl和jq的即用型镜像。现在是时候使用Dockerfile来添加一个钩子:


$ cat Dockerfile
FROM flant/shell-operator:v1.0.0-beta.1-alpine3.9
ADD namespace-hook.sh /hooks
$ docker build -t registry.example.com/my-operator:v1 .
$ docker push registry.example.com/my-operator:v1

在集群中运行


我们再来看看这个钩子,这次我们将关注具体的操作以及它在集群中执行的对象:



  1. 它订阅了namespace的创建事件;

  2. 它在不与它所运行的命名空间相同的空间创建一个secret。


这里我们会发现运行这个镜像的Pod需要有执行这些操作的权限。你可以授权给一个ServiceAccount。由于我们是关注整个集群中的对象,那么权限需要使用ClusterRoleClusterRoleBinding形式来配置。


YAML最终配置描述如下:


---
apiVersion: v1
kind: ServiceAccount
metadata:
name: monitor-namespaces-acc
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: monitor-namespaces
rules:
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["get", "watch", "list"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "create", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: monitor-namespaces
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: monitor-namespaces
subjects:
- kind: ServiceAccount
name: monitor-namespaces-acc
namespace: example-monitor-namespaces

你可以将创建的镜像部署为一个简单的Deployment:


apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: my-operator
spec:
template:
spec:
containers:
- name: my-operator
image: registry.example.com/my-operator:v1
serviceAccountName: monitor-namespaces-acc

为方便起见,我们将创建一个单独的命名空间,用于运行shell-operator并应用创建的部署清单:


$ kubectl create ns example-monitor-namespaces
$ kubectl -n example-monitor-namespaces apply -f rbac.yaml
$ kubectl -n example-monitor-namespaces apply -f deployment.yaml

好了,shell-operator启动,它将订阅命名空间创建事件并在需要时执行钩子。



这样一个简单的shell脚本就变成了Kubernetes中一个真正的Operator,并成为集群的一部分。这样做的好处是我们避免了使用Golang来开发Operator的复杂过程:




过滤


关于对象的观察很棒,但我们通常需要响应对象中某些属性的更改,例如,增加/减少部署中的副本数量或对象对标签中的任何更新。


当一个事件发生时,shell-operator接收该对象的JSON清单。在此JSON中,你可以选择要监视的属性,并仅在更改时启动钩子。jqFilter字段可以帮助你完成这点:你应该输入将应用于JSON清单的jq表达式。


举个例子,要响应Deployment对象标签中的修改,你必须从metadata字段中提取labels字段。这个例子中你将需要如下的配置:


cat <

jqFilter表达式将Deployment的长长的JSON清单转换成带有标签的简短的JSON:




shell-operator将只会在这个简短的JSON发生变化时执行钩子。其它属性的变更将会被忽略。


钩子的执行上下文


钩子的配置允许你指定几种事件。例如你可以定义两个Kubernetes事件和两个计划调度:


{
"onKubernetesEvent": [
{
"name": "OnCreatePod",
"kind": "pod",
"event": [
"add"
]
},
{
"name": "OnModifiedNamespace",
"kind": "namespace",
"event": [
"update"
],
"jqFilter": ".metadata.labels"
}
],
"schedule": [
{
"name": "every 10 min",
"crontab": "0 */10 * * * *"
},
{
"name": "on Mondays at 12:10",
"crontab": "0 10 12 * * 1"
}
]
}

注意:shell-operator支持以crontab样式运行脚本!你可以在文档中找到额外的信息。


为了区分钩子执行的原因,shell-operator会创建一个临时文件并将其路径保存到BINDING_CONTEXT_TYPE变量中。此文件包含了执行钩子的原因的JSON描述。例如,每隔10分钟将会使用以下内容启动钩子:


[{ "binding": "every 10 min" }]

在周一的话它将以以下内容启动:


[{ "binding": "every 10 min" }, { "binding": "on Mondays at 12:10" }]

同时将有onKubernetesEvent调用的更详细的JSON,因为它包含了对象的描述:


[
{
"binding": "onCreatePod",
"resourceEvent": "add",
"resourceKind": "pod",
"resourceName": "foo",
"resourceNamespace": "bar"
}
]

你能通过名称来全面了解字段的内容(更多详细信息可在文档中找到)。使用jq从resourceName获取资源名称的示例已经在复制secret的钩子中展示:


jq -r '.[0].resourceName' $BINDING_CONTEXT_PATH

你可以通过类似的方式去获取到其它字段。


下一步是什么呢?


在该项目仓库中的/examples directory目录里包含了一些可以直接在集群中使用的示例。你可以将它们用作你开发自己的钩子的基础。


Shell-operator同样支持使用Prometheus来收集指标。METRICS章节已描述了这些可用的指标。


你能轻易想到,shell-operator是使用Go编写的,并根据开源许可证(Apache 2.0)的条款进行分发。我们非常感谢任何关于开发在Github上的这个项目的帮助。你可以通过给我们点Star,反馈问题或者是PR来支持我们!


原文链接:Announcing shell-operator to simplify creating of Kubernetes operators(翻译:冯旭松)


在Docker上编译OpenJDK 8

李颖杰 发表了文章 • 0 个评论 • 136 次浏览 • 2019-06-18 11:06 • 来自相关话题

以前曾经试过在VMware上安装Linux,但是每次都不顺利,例如Linux环境,预装依赖软件,OpenJDK源码的选择等环境都会遇到问题,一旦失败再重新开始挺费时间的,现在用Docker就省事多了,镜像和容器的创建和删除都比较简单干净,专心做好Dock ...查看全部

以前曾经试过在VMware上安装Linux,但是每次都不顺利,例如Linux环境,预装依赖软件,OpenJDK源码的选择等环境都会遇到问题,一旦失败再重新开始挺费时间的,现在用Docker就省事多了,镜像和容器的创建和删除都比较简单干净,专心做好Dockerfile就行,下面我们一起来实践一下吧。



本次实战用到的所有文件,已经打包到GitHub上,路径是git@github.com:zq2599/centos7_build_openjdk8.git,欢迎您来使用(git clone git@github.com:zq2599/centos7_build_openjdk8.git)。



本次编译实战的基本步骤如下:


编写的Dockerfile中要做的如下的事情:




  1. 安装依赖的软件;

  2. 把OpenJDK的源码复制到镜像中;



在编写Dockerfile之前要做三个重要的选择,如下:




  1. Linux:我选择了CentOS 7,之前试过Ubuntu 16.04,但是在Configure的时候提示”freetype”没有安装,我按照提示去装了,再次Configure的时候继续提示”freetype”没有安装……(此问题现在还没解决,如果您解决过相同问题,请您告诉一下解决方法,谢谢了!)

  2. OpenJDK源码,这次要编译的是OpenJDK 8,源码的下载地址在这里

  3. Bootstrap JDK:即编译时要用到的JDK,下载了OpenJDK的源码后,解压开可以看到“README-builds.html”这个文件,里面有对Bootstrap JDK的描述:




看得出,需要安装JDK 7来编译OpenJDK 8的源码。



OK,关键问题都已确认,即将开始Dockerfile制作,不过制作之前还有个小问题需要先想好:本次我打算把制作镜像所需的Dockerfile和依赖文件都放到GitHub上去,这样做的好处有两个:




  1. 读者们从Git上clone下来之后直接执行Docker build就能在本地构建镜像;

  2. daocloud.io网站上支持通过执行GitHub目录的方式在线构建镜像,后面我们会实践在daocloud.io上构建镜像并部署到腾讯云或者阿里云服务器上。



上传到Gith=Hub时,除了Dockerfile,还要上传的文件有两个:JDK 1.7和OpenJDK 8源码,都超过了100M,如下图:


这就麻烦了,GitHub上传文件的时候,单个文件不能超过100M,否则push的时候会被服务器拒绝,解决这个问题有两个办法:



1、构建镜像的时候不要把这两个文件复制到镜像中了,改为在Dockerfile中通过执行wget命令将这两个文件分别下载到镜像中,OpenJDK的下载路径是:http://www.java.net/download/openjdk/jdk8/promoted/b132/openjdk-8-src-b132-03_mar_2014.zip,而OpenJDK 1.7的下载路径就难办了,Oracle上下载历史版本的时候,是要做登录操作的,这个在Dockerfile中难以实现,找到了一个下载地址:https://mirror.its.sfu.ca/mirror/CentOS-Third-Party/NSG/common/x86_64/jdk-7u80-linux-x64.rpm,但是下载速度很慢,最少一个小时以上了,所以wget这种方法看似简单,但下载文件耗时实在太长;



2、第二种方法比较简单易用,就是在Mac或者Linux上先用split命令将文件分割成多个,再上传到GitHub上,在Dockerfile中有对应的命令将分割后的文件恢复成分割前的原文件,具体的分割命令如下:





split -b 50m jdk-7u71-linux-x64.rpm jdkrpm-


这个命令是将jdk-7u71-linux-x64.rpm分割成不超过50m的多个文件,分割后的文件以jdkrpm-作为文件名的前缀,如下图:


OpenJDK的源码用如下命令分割:





split -b 50m openjdk-8-src-b132-03_mar_2014.zip openjdksrc-


Dockerfile中,从分割文件恢复以上两个原文件的命令为:





cat jdkrpm-* > jdk-7u71-linux-x64.rpm
cat openjdksrc-* > openjdk-8-src-b132-03_mar_2014.zip


以上就是两种处理大文件的方法,本文用的是第二种,即先分割上传到Git,在Dockerfile中将已分割文件恢复成原文件再使用。



好了,前期的准备工作已经做完了,现在可以编写Dockerfile文件了,整个文件中要做的事情列出如下:



1、安装依赖软件,例如libXtst-devel,libXt-devel等等,这些都是编译前的Configure命令要检查的,检查不过无法进行编译;



2、把分割后的OpenJDK源码复制到镜像文件中,再合成,再解压;



3、安装JDK 7,把分割后的安装文件复制到镜像中合成,然后安装;



4、为了方便用户进入容器后快速开始编译,我们做了一个shell脚本start_make.sh,把这个脚本也要复制到镜像中,内容如下:





#!/bin/bash
$WORK_PATH/$OPENJDK_SRC_DIR/configure
echo "start make"
cd $WORK_PATH/$OPENJDK_SRC_DIR
make all ZIP_DEBUGINFO_FILES=0 DISABLE_HOTSPOT_OS_VERSION_CHECK=OK


5、清理无用的文件,例如OpenJDK源码的压缩文件,JDK 7的安装文件等;



按照以上步骤,最终写出的Dockerfile文件如下:





# Docker image of compile and build enviroment for openjdk8
# VERSION 0.0.1
# Author: bolingcavalry

#基础镜像使用CentOS 7
FROM centos:centos7

#作者
MAINTAINER BolingCavalry

#定义工作目录
ENV WORK_PATH /usr/local

#定义JDK 1.7的文件名
ENV JDK_RPM_FILE jdk-7u71-linux-x64.rpm

#定义OpenJDK源码的文件名
ENV OPENJDK_SRC_ZIP openjdk-8-src-b132-03_mar_2014.zip

#定义解压缩后的文件名
ENV OPENJDK_SRC_DIR openjdk

#yum更新
RUN yum -y update

#安装工具集
RUN yum -y groupinstall "Development Tools"

#安装即将用到的软件
RUN yum -y install unzip libXtst-devel libXt-devel libXrender-devel cups-devel freetype-devel alsa-lib-devel which

#把分割过的JDK 1.7安装文件复制到工作目录
COPY ./jdkrpm-* $WORK_PATH/

#用本地分割过的文件恢复原有的JDK 1.7的安装文件
RUN cat $WORK_PATH/jdkrpm-* > $WORK_PATH/$JDK_RPM_FILE

#本地安装JDK 1.7
RUN yum -y localinstall $WORK_PATH/$JDK_RPM_FILE

#把分割过的OpenJDK 8的源码压缩包复制到工作目录
COPY ./openjdksrc-* $WORK_PATH/

#用本地分割过的文件恢复原有的OpenJDK 8的源码压缩包
RUN cat $WORK_PATH/openjdksrc-* > $WORK_PATH/$OPENJDK_SRC_ZIP

#解压缩源码
RUN unzip $WORK_PATH/$OPENJDK_SRC_ZIP -d $WORK_PATH

#复制启动编译的shell
COPY ./start_make.sh $WORK_PATH/$OPENJDK_SRC_DIR/

#给执行文件增加可执行权限:Configure文件
RUN chmod a+x $WORK_PATH/$OPENJDK_SRC_DIR/configure

#给执行文件增加可执行权限:启动编译文件
RUN chmod a+x $WORK_PATH/$OPENJDK_SRC_DIR/start_make.sh

#删除分割文件
RUN rm $WORK_PATH/jdkrpm-*

#删除分割文件
RUN rm $WORK_PATH/openjdksrc-*

#删除JDK安装包文件
RUN rm $WORK_PATH/$JDK_RPM_FILE

#删除OpenJDK源码压缩文件
RUN rm $WORK_PATH/$OPENJDK_SRC_ZIP


至此,镜像文件制作所需的材料都已经齐全了,如下图:


现在让我们开始制作镜像吧,打开终端,进入Dockerfile所在目录,执行命令:





docker build -t bolingcavalryopenjdk:0.0.1 .


因为要在线安装不少的软件,所以可能耗时会略长,和网络带宽有关,我在住处用家庭网络大概10分钟左右构建成功,执行目录docker images查看镜像,新的镜像文件已经生成了,如下图:


现在启动一个容器试试吧:





docker run --name=jdk001 -idt bolingcavalryopenjdk:0.0.1


容器已经启动,再执行以下命令进入容器:





docker exec -it jdk001 /bin/bash


进去后,直接到/usr/local/openjdk目录下,执行./start_make.sh,开始编译了,整个过程的耗时和当前电脑的硬件配置有关,我用i7处理器的Mac Pro 15大约要用20多分钟,编译结束后会有类似下图的输出:


这时候去/usr/local/openjdk目录下看看,发现多了一个build目录,这里面就是编译好的结果,如下图:


build目录下只有一个linux-x86_64-normal-server-release目录,再进去就能看到JDK目录了,如下图:


进入/usr/local/openjdk/build/linux-x86_64-normal-server-release/jdk/bin目录,会发现里面有Java文件,执行./java -version输出如下:


新的JDK信息已经打印出来了,OpenJDK Runtime Enviroment信息已经说明了这是个最新构建的JDK环境。



至此我们的本次实战就结束了,面对如此干净的编译环境和现成的源码,读者您是否有一种改动一番源码的冲动,然后构建一个个性化的属于自己的JDK,建议您自己动手修改和编译OpenJDK源码,以加深对JVM的认识。

苹果宣布加入云原生计算基金会

大卫 发表了文章 • 0 个评论 • 408 次浏览 • 2019-06-13 08:27 • 来自相关话题

云原生计算基金会(简称CNCF)作为Kubernetes等顶级开源项目的运营方,今天宣布苹果公司即将以最高级白金用户的身份正式加盟。凭借这位新成员,CNCF如今已经拥有89位最终用户成员,其中还包括阿迪达斯、Atlassian、Box、GitHub、纽约 ...查看全部

云原生计算基金会(简称CNCF)作为Kubernetes等顶级开源项目的运营方,今天宣布苹果公司即将以最高级白金用户的身份正式加盟。凭借这位新成员,CNCF如今已经拥有89位最终用户成员,其中还包括阿迪达斯、Atlassian、Box、GitHub、纽约时报、Reddit、Spotify以及沃尔玛等。

与以往的作风一样,苹果公司并没有对此次公告做出评论。但CNCF方面指出,最终用户资格主要面向那些“对开源云原生技术高度依赖”且希望回馈整个社区的重度用户。作为CNCF最终用户成员,苹果公司实际上也正式加入了Linux基金会。

作为成员关系的一部分,苹果公司还在CNCF管理委员会当中获得一个席位,苹果方面的高级工程技术经理Tomer Doron将出任这一职位。

云原生计算基金会CTO Chris Aniszczyk表示,“吸引到苹果这样一家拥有丰富经验与庞大业务规模的企业作为最终用户成员,充分证明了云原生计算在未来基础设施与应用程序开发方面的可观潜力。我们很高兴能够获得苹果公司的支持,并期待着未来能够为更为广泛的云原生项目社区做出更多贡献。”

虽然很多朋友可能不会把苹果公司与开源参与企业联系起来,但事实上该公司确实已经开放了从Darwin操作系统的XNU内核到Swift编程语言等一系列内部成果。话虽如此,苹果方面一般不会参与开源云基础设施社区,而这次的举动可能预示着这位消费电子巨头正在迎来转变。苹果公司肯定拥有自己的数据中心,因此其确实有可能高度依赖着各类开源基础设施项目——当然,按照苹果的一贯风格,其对这类话题往往避而不谈。

原文链接:Apple joins the open-source Cloud Native Computing Foundation

基于Jenkins Pipeline自动化部署

尼古拉斯 发表了文章 • 0 个评论 • 484 次浏览 • 2019-06-05 23:53 • 来自相关话题

最近在公司推行Docker Swarm集群的过程中,需要用到Jenkins来做自动化部署,Jenkins实现自动化部署有很多种方案,可以直接在jenkins页面写Job,把一些操作和脚本都通过页面设置,也可以在每个项目中直接写Pipeline脚本,但像我 ...查看全部

最近在公司推行Docker Swarm集群的过程中,需要用到Jenkins来做自动化部署,Jenkins实现自动化部署有很多种方案,可以直接在jenkins页面写Job,把一些操作和脚本都通过页面设置,也可以在每个项目中直接写Pipeline脚本,但像我那么追求极致的程序员来说,这些方案都打动不了我那颗骚动的心,下面我会跟你们讲讲我是如何通过Pipeline脚本实现自动化部署方案的,并且实现多分支构建,还实现了所有项目共享一个Pipeline脚本。
#使用Jenkins前的一些设置

为了快速搭建Jenkins,我这里使用Docker安装运行Jenkins:

$ sudo docker run -it -d \
--rm \
-u root \
-p 8080:8080 \
-v jenkins-data:/var/jenkins_home \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "$HOME":/home \
--name jenkins jenkinsci/blueocean

初次使用Jenkins,进入Jenkins页面前,需要密码验证,我们需要进入Docker容器查看密码:
$ sudo docker exec -it jenkins /bin/bash
$ vi /var/jenkins_home/secrets/initialAdminPassword

Docker安装的Jenkins稍微有那么一点缺陷,shell版本跟CentOS宿主机的版本不兼容,这时我们需要进入Jenkins容器手动设置shell:
$ sudo docker exec -it jenkins /bin/bash
$ ln -sf /bin/bash /bin/sh

由于我们的Pipeline还需要在远程服务器执行任务,需要通过SSH连接,那么我们就需要在Jenkins里面生成SSH的公钥密钥:
$ sudo docker exec -it jenkins /bin/bash
$ ssh-keygen -C "root@jenkins"

在远程节点的~/.ssh/authorized_keys中添加jenkins的公钥(id_rsa.pub)。

还需要安装一些必要的插件:

  1. Pipeline Maven Integration
  2. SSH Pipeline Steps

安装完插件后,还需要去全局工具那里添加Maven:


这里后面Jenkinsfile有用到。
#mutiBranch多分支构建

由于我们的开发是基于多分支开发,每个开发环境都对应有一条分支,所以普通的Pipeline自动化构建并不能满足现有的开发部署需求,所以我们需要使用Jenkins的mutiBranch Pipeline。如果你想和更多Jenkins技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态

首先当然是新建一个mutiBranch多分支构建Job:


接着设置分支源,分支源就是你项目的Git地址,选择Jenkinsfile在项目的路径:


接下来Jenkins会在分支源中扫描每个分支下的Jenkinsfile,如果该分支下有Jenkinsfile,那么就会创建一个分支Job:


该Job下的分支Job如下:


这里需要注意的是,只有需要部署的分支,才加上Jenkinsfile,不然Jenkins会将其余分支也创建一个分支Job。
#通用化Pipeline脚本

到这里之前,基本就可以基于Pipeline脚本自动化部署了,但如果你是一个追求极致,不甘于平庸的程序员,你一定会想,随着项目的增多,Pipeline脚本不断增多,这会造成越来越大的维护成本,业务的极速增长难免会在脚本中修改东西,这就会牵扯太多Pipeline脚本的改动,而且这些脚本基本都相同,那么对于我这么优秀的程序员,怎么会想不到这个问题呢,我第一时间就想到通用化Pipeline脚本。所幸,Jenkins已经看出了我不断骚动的心了,Jenkins甩手就给我一个Shared Libraries。

Shared Libraries是什么呢?顾名思义,它就是一个共享库,它的主要作用是用于将通用的Pipeline脚本放在一个地方,其它项目可以从它那里获取到一个全局通用化的Pipeline脚本,项目之间通过不通的变量参数传递,达到通用化的目的。

接下来我们先创建一个用于存储通用Pipeline脚本的Git仓库:


仓库目录就不能随便乱添加了,Jenkins有一个严格的规范,下面是官方说明:


官方已经讲得很清楚了,大概意思就是vars目录用于存储通用Pipeline脚本,resources用于存储非Groovy文件。所以我这里就把Pipeline需要的构建脚本以及编排文件都集中放在这里,完全对业务工程师隐蔽,这样做的目的就是为了避免业务工程师不懂瞎几把乱改,导致出bug。

创建完Git仓库后,我们还需要在Jenkins的Manage Jenkins » Configure System » Global Pipeline Libraries中定义全局库:


这里的name,可以在jenkinsfile中通过以下命令引用:

@Library 'objcoding-pipeline-library'

下面我们来看通用Pipeline脚本的编写规则:
#!groovy

def getServer() {
def remote = [:]
remote.name = 'manager node'
remote.user = 'dev'
remote.host = "${REMOTE_HOST}"
remote.port = 22
remote.identityFile = '/root/.ssh/id_rsa'
remote.allowAnyHosts = true
return remote
}

def call(Map map) {

pipeline {
agent any

environment {
REMOTE_HOST = "${map.REMOTE_HOST}"
REPO_URL = "${map.REPO_URL}"
BRANCH_NAME = "${map.BRANCH_NAME}"
STACK_NAME = "${map.STACK_NAME}"
COMPOSE_FILE_NAME = "" + "${map.STACK_NAME}" + "-" + "${map.BRANCH_NAME}" + ".yml"
}

stages {
stage('获取代码') {
steps {
git([url: "${REPO_URL}", branch: "${BRANCH_NAME}"])
}
}

stage('编译代码') {
steps {
withMaven(maven: 'maven 3.6') {
sh "mvn -U -am clean package -DskipTests"
}
}
}

stage('构建镜像') {
steps {
sh "wget -O build.sh https://git.x-vipay.com/docker/jenkins-pipeline-library/raw/master/resources/shell/build.sh"
sh "sh build.sh ${BRANCH_NAME} "
}
}

stage('init-server') {
steps {
script {
server = getServer()
}
}
}

stage('执行发版') {
steps {
writeFile file: 'deploy.sh', text: "wget -O ${COMPOSE_FILE_NAME} " +
" https://git.x-vipay.com/docker/jenkins-pipeline-library/raw/master/resources/docker-compose/${COMPOSE_FILE_NAME} \n" +
"sudo docker stack deploy -c ${COMPOSE_FILE_NAME} ${STACK_NAME}"
sshScript remote: server, script: "deploy.sh"
}
}
}
}
}


  1. 由于我们需要在远程服务器执行任务,所以定义一个远程服务器的信息其中remote.identityFile就是我们上面在容器生成的密钥的地址;
  2. 定义一个call()方法,这个方法用于在各个项目的Jenkinsfile中调用,注意一定得叫call;
  3. 在call()方法中定义一个Pipeline;
  4. environment参数即是可变通用参数,通过传递参数Map来给定值,该Map是从各个项目中定义的传参;
  5. 接下来就是一顿步骤操作啦,“编译代码”这步骤需要填写上面我们在全局工具类设置的maven,“构建镜像”的构建脚本巧妙地利用wget从本远程仓库中拉取下来,”执行发版“的编排文件也是这么做,“init-server”步骤主要是初始化一个server对象,供“执行发版使用”。

从脚本看出来Jenkins将来要推崇的一种思维:配置即代码。

写完通用Pipeline脚本后,接下来我们就需要在各个项目的需要自动化部署的分支的根目录下新建一个Jenkinsfile脚本了:


接下来我来解释一下Jenkinsfile内容:

#!groovy

// 在多分支构建下,严格规定Jenkinsfile只存在可以发版的分支上

// 引用在jenkins已经全局定义好的library
library 'objcoding-pipeline-library'
def map = [:]

// 远程管理节点地址(用于执行发版)
map.put('REMOTE_HOST','xxx.xx.xx.xxx')
// 项目gitlab代码地址
map.put('REPO_URL','https://github.com/objcoding/docker-jenkins-pipeline-sample.git')
// 分支名称
map.put('BRANCH_NAME','master')
// 服务栈名称
map.put('STACK_NAME','vipay')

// 调用library中var目录下的build.groovy脚本
build(map)


  1. 通过library 'objcoding-pipeline-library'引用我们在Jenkins定义的全局库,定义一个map参数;
  2. 接下来就是将项目具体的参数保存到map中,调用build()方法传递给通用Pipeline脚本。

Shared Libraries共享库极大地提升了Pipeline脚本的通用性,避免了脚本过多带来的问题,也符合了一个优秀程序员的审美观,如果你是一个有追求的程序员,你一定会爱上它。

附上一张价值连城的手稿图:



原文链接:https://mp.weixin.qq.com/s/2M2RQN-_2wmWf4OslIpuaA

DockOne微信分享(二一一):基于Actor模型的CQRS/ES解决方案分享

JetLee 发表了文章 • 0 个评论 • 442 次浏览 • 2019-06-05 16:17 • 来自相关话题

【编者的话】2017年加密货币比较流行,我曾有幸在加密货币交易所参与开发工作,为了应对交易系统高性能、高并发、高可用的要求,我们使用基于Actor模型的Orleans技术,采用CQRS/ES架构结合Service Fabric开展了富有挑战性的工作。本文 ...查看全部

【编者的话】2017年加密货币比较流行,我曾有幸在加密货币交易所参与开发工作,为了应对交易系统高性能、高并发、高可用的要求,我们使用基于Actor模型的Orleans技术,采用CQRS/ES架构结合Service Fabric开展了富有挑战性的工作。本文将从不同视角为大家介绍Actor模型、CQRS/ES架构以及Service Fabric在高并发场景中的考量和应用。



话题由三部分组成:




  • Actor模型&Orleans:在编程的层面,从细粒度-由下向上的角度介绍Actor模型;

  • CQRS/ES:在框架的层面,从粗粒度-由上向下的角度介绍Actor模型,说明Orleans技术在架构方面的价值;

  • Service Fabric:从架构部署的角度将上述方案落地上线。



群里的小伙伴技术栈可能多是Java和Go体系,分享的话题主要是C#技术栈,没有语言纷争,彼此相互学习。比如:Scala中,Actor模型框架有akka,CQRS/ES模式与编程语言无关,Service Fabric与Kubernetes是同类平台,可以相互替代,我自己也在学习Kubernetes。





Actor模型&Orleans(细粒度)





共享内存模型



多核处理器出现后,大家常用的并发编程模型是共享内存模型。


这种编程模型的使用带来了许多痛点,比如:




  • 编程:多线程、锁、并发集合、异步、设计模式(队列、约定顺序、权重)、编译

  • 无力:单系统的无力性:①地理分布型、②容错型

  • 性能:锁,性能会降低

  • 测试:



    • 从坑里爬出来不难,难的是我们不知道自己是不是在坑里(开发调试的时候没有热点可能是正常的)

    • 遇到bug难以重现。有些问题特别是系统规模大了,可能运行几个月才能重现问题

  • 维护:



    • 我们要保证所有对象的同步都是正确的、顺序的获取多个锁。

    • 12个月后换了另外10个程序员仍然按照这个规则维护代码。



简单总结:




  • 并发问题确实存在

  • 共享内存模型正确使用掌握的知识量多

  • 加锁效率就低

  • 存在许多不确定性





Actor模型



Actor模型是一个概念模型,用于处理并发计算。Actor由3部分组成:状态(State)+行为(Behavior)+邮箱(Mailbox),State是指Actor对象的变量信息,存在于Actor之中,Actor之间不共享内存数据,Actor只会在接收到消息后,调用自己的方法改变自己的state,从而避免并发条件下的死锁等问题;Behavior是指Actor的计算行为逻辑;邮箱建立Actor之间的联系,一个Actor发送消息后,接收消息的Actor将消息放入邮箱中等待处理,邮箱内部通过队列实现,消息传递通过异步方式进行。


Actor是分布式存在的内存状态及单线程计算单元,一个Id对应的Actor只会在集群种存在一个(有状态的 Actor在集群中一个Id只会存在一个实例,无状态的可配置为根据流量存在多个),使用者只需要通过Id就能随时访问不需要关注该Actor在集群的什么位置。单线程计算单元保证了消息的顺序到达,不存在Actor内部状态竞用问题。



Actor是分布式存在的内存状态及单线程计算单元,一个ID对应的Actor只会在集群种存在一个(有状态的 Actor在集群中一个ID只会存在一个实例,无状态的可配置为根据流量存在多个),使用者只需要通过ID就能随时访问不需要关注该Actor在集群的什么位置。单线程计算单元保证了消息的顺序到达,不存在Actor内部状态竞用问题。



举个例子:



多个玩家合作在打Boss,每个玩家都是一个单独的线程,但是Boss的血量需要在多个玩家之间同步。同时这个Boss在多个服务器中都存在,因此每个服务器都有多个玩家会同时打这个服务器里面的Boss。



如果多线程并发请求,默认情况下它只会并发处理。这种情况下可能造成数据冲突。但是Actor是单线程模型,意味着即使多线程来通过Actor ID调用同一个Actor,任何函数调用都是只允许一个线程进行操作。并且同时只能有一个线程在使用一个Actor实例。





Actor模型:Orleans



Actor模型这么好,怎么实现?



可以通过特定的Actor工具或直接使用编程语言实现Actor模型,Erlang语言含有Actor元素,Scala可以通过Akka框架实现Actor编程。C#语言中有两类比较流行,Akka.NET框架和Orleans框架。这次分享内容使用了Orleans框架。



特点:



Erlang和Akka的Actor平台仍然使开发人员负担许多分布式系统的复杂性:关键的挑战是开发管理Actor生命周期的代码,处理分布式竞争、处理故障和恢复Actor以及分布式资源管理等等都很复杂。Orleans简化了许多复杂性。



优点:




  • 降低开发、测试、维护的难度

  • 特殊场景下锁依旧会用到,但频率大大降低,业务代码里甚至不会用到锁

  • 关注并发时,只需要关注多个actor之间的消息流

  • 方便测试

  • 容错

  • 分布式内存



缺点:




  • 也会出现死锁(调用顺序原因)

  • 多个Actor不共享状态,通过消息传递,每次调用都是一次网络请求,不太适合实施细粒度的并行

  • 编程思维需要转变






第一小节总结:上面内容由下往上,从代码层面细粒度层面表达了采用Actor模型的好处或原因。





CQRS/ES(架构层面)





从1000万用户并发修改用户资料的假设场景开始







  1. 每次修改操作耗时200ms,每秒5个操作

  2. MySQL连接数在5K,分10个库

  3. 5 *5k *10=25万TPS

  4. 1000万/25万=40s

 

在秒杀场景中,由于对乐观锁/悲观锁的使用,推测系统响应时间更复杂。





使用Actor解决高并发的性能问题





1000万用户,一个用户一个Actor,1000万个内存对象。


200万件SKU,一件SKU一个Actor,200万个内存对象。




  • 平均一个SKU承担1000万/200万=5个请求

  • 1000万对数据库的读写压力变成了200万

  • 1000万的读写是同步的,200万的数据库压力是异步的

  • 异步落盘时可以采用批量操作



总结:



由于1000万+用户的请求根据购物意愿分散到200万个商品SKU上:每个内存领域对象都强制串行执行用户请求,避免了竞争争抢;内存领域对象上扣库存操作处理时间极快,基本没可能出现请求阻塞情况。



从架构层面彻底解决高并发争抢的性能问题。理论模型,TPS>100万+……





EventSourcing:内存对象高可用保障



Actor是分布式存在的内存状态及单线程计算单元,采用EventSourcing只记录状态变化引发的事件,事件落盘时只有Add操作,上述设计中很依赖Actor中State,事件溯源提高性能的同时,可以用来保证内存数据的高可用。






CQRS



上面1000万并发场景的内容来自网友分享的PPT,与我们实际项目思路一致,就拿来与大家分享这个过程,下图是我们交易所项目中的架构图:


开源版本架构图:


开源项目github:https://github.com/RayTale/Ray



第二小节总结:由上往下,架构层面粗粒度层面表达了采用Actor模型的好处或原因。





Service Fabric



系统开发完成后Actor要组成集群,系统在集群中部署,实现高性能、高可用、可伸缩的要求。部署阶段可以选择Service Fabric或者Kubernetes,目的是降低分布式系统部署、管理的难度,同时满足弹性伸缩。



交易所项目可以采用Service Fabric部署,也可以采用Kubernetes,当时Kubernetes还没这么流行,我们采用了Service Fabric,Service Fabric 是一款微软开源的分布式系统平台,可方便用户轻松打包、部署和管理可缩放的可靠微服务和容器。开发人员和管理员不需解决复杂的基础结构问题,只需专注于实现苛刻的任务关键型工作负荷,即那些可缩放、可靠且易于管理的工作负荷。支持Windows与Linux部署,Windows上的部署文档齐全,但在Linux上官方资料没有。现在推荐Kubernetes。



第三小节总结:




  1. 借助Service Fabric或Kubernetes实现低成本运维、构建集群的目的。

  2. 建立分布式系统的两种最佳实践:



    • 进程级别:容器+运维工具(Kubernetes/Service Fabric)

    • 线程级别:Actor+运维工具(Kubernetes/Service Fabric)

参考:




  1. ES/CQRS部分内容参考:《领域模型 + 内存计算 + 微服务的协奏曲:乾坤(演讲稿)》2017年互联网应用架构实战峰会

  2. 其他细节来自互联网,不一一列出





Q&A



Q:单点故障后,正在处理的cache数据如何处理的,例如,http、tcp请求……毕竟涉及到钱?

A:Actor有激活和失活的生命周期,激活的时候使用快照和Events来恢复最新内存状态,失活的时候保存快照。Actor框架保证系统中同一个key只会存在同一个Actor,当单点故障后,Actor会在其它节点重建并恢复最新状态。



Q:数据落地得策略是什么?还是说就是直接落地?

A:event数据直接落地;用于支持查询的数据,是Handler消费event后异步落库。



Q:Grain Persistence使用Relational Storage容量和速度会不会是瓶颈?

A:Grain Persistence存的是Grain的快照和event,event是只增的,速度没有出现瓶颈,而且开源版本测试中PostgreSQL性能优于MongoDB,在存储中针对这两个方面做了优化:比如分表、归档处理、快照处理、批量处理。



Q:Orleans中,持久化事件时,是否有支持并发冲突的检测,是如何实现的?

A:Orleans不支持;工作中,在事件持久化时做了这方面的工作,方式是根据版本号。



以上内容根据2019年6月4日晚微信群分享内容整理。分享人郑承良,上海某科技公司架构师,对高并发场景下的分布式金融系统拥有丰富的实战经验,曾为澳大利亚、迪拜多家交易所提供技术支持。DockOne每周都会组织定向的技术分享,欢迎感兴趣的同学加微信:liyingjiese,进群参与,您有想听的话题或者想分享的话题都可以给我们留言。

使用Docker高效搭建开发环境

齐达内 发表了文章 • 0 个评论 • 498 次浏览 • 2019-06-04 23:00 • 来自相关话题

【编者的话】Docker作为轻量级的基于容器的解决方案,它对系统侵入性低,容易移植,天生就适合做复杂业务部署和开发环境搭建,今天给大家带来的是奇虎360开发是如何使用Docker高效搭建开发环境的。 作为一个平时喜 ...查看全部

【编者的话】Docker作为轻量级的基于容器的解决方案,它对系统侵入性低,容易移植,天生就适合做复杂业务部署和开发环境搭建,今天给大家带来的是奇虎360开发是如何使用Docker高效搭建开发环境的。



作为一个平时喜欢折腾的开发人员,我喜欢尝试各种环境,使用感兴趣的各种开源软件。



同时,我也是有一些相对的小洁癖,很喜欢Linux中权限最小化原则,我也不喜欢自己的环境中有太多不知道的东西。



做了多年的Web开发,我接触到的环境大致如下:




  1. 操作系统从CentOS 5到CentOS 7;

  2. Web Server从Apache到Nginx;

  3. 开发语言从最初的PHP 5.2到PHP 7,又到现在主要使用Go,马上还会开始接触C++;

  4. 数据库从MySQL 5.1到现在的5.7,前阵子又开始折腾MariaDB;

  5. Cache选型从Memcache到Redis;

  6. 队列用过Kafka,去年开始大量使用NSQ;



公司虽然有专门负责部署、运维这些服务的同学,但我在开发的时候,还是喜欢自己来搭建这些东西,因为这样通常可以对使用到的服务有更多的认识,也能帮助自己使用的更好。如果你想和更多Docker技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态



今天我就来和大家分享下我是如何高效的搭建好自己的开发环境的。





搭建前说明



这里先说明一点,对每个开源软件,我几乎都是自己编译部署的,而不会使用类似yum install这种方式,也很少直接下载官方编译好的二进制包,这都是为了能多深入了解用到的开源软件。



但一些依赖的动态库文件,如zlib等,还有编译工具,如GCC、make等,我都是通过方便的yum install这种方式直接安装的,否则会累死。





传统做法



我在很长的一段时间内,都是把每个软件的编译、安装过程写成一个脚本,之后再需要用的时候直接运行脚本即可,但这样的方式,通常会遇到下面这些问题:




  1. 脚本只能在我当时的操作系统环境下运行。记得当时购买过不同服务商的VPS,虽然不同VPS我都使用同样的Linux发行版,但脚本通常都不能一键跑完。这也是没办法,因为每个VPS服务商都会制作自己的操作系统镜像版本。

  2. 操作系统升级,如CentOS 5 - 6,或是换为Ubuntu,这样基本上脚本都跑不了。

  3. 软件升级,如MySQL 5.2 - 5.6,构建工具改为CMake,依赖库改变或升级。

  4. 如果某个软件依赖的公共库版本和其它软件不同,且公共库升级后和旧版不兼容,那你就只能为这个软件单独编译公共库了,如果只是普通的公共库还好,但如果是所需要的编译工具版本不同,那可就惨了。



上面这些问题,如果你想每个发行版维护一个脚本,那会累死,因为一旦你每次想升级一个软件,难道每个发行版都要编译一遍吗?这就变成了收获价值很低的体力劳动了。



由于喜欢折腾的个性,我对操作系统的升级以及软件包版本的升级又经常发生,所以一直以来,我都在寻找一个好方法,能很方便的维护好自己的开发环境,尽量做到只=新东西只为它工作一次,最后我找到了Docker,目前我都是用它来搭建自己的开发环境的。





Docker做法



先概括介绍下我的方法:




  1. 让每个软件运行在容器中,因为运行的容器环境是可以固定下来的,所以编译安装脚本写一个就可以了。

  2. 代码使用数据卷的方式加载到需要的容器中。

  3. 因为是开发环境,所以网络方面使用最简单的–net=host。

  4. 将镜像的创建、容器的启动维护在Git项目中,并抽象出统一的构建过程,很方面的做到新软件接入,新机器部署。



下面用实例来说明把:





示例Nginx环境构建



我将构建过程放到Git中:https://gitee.com/andals/docker-nginx



Readme中记录了构建所需要执行的脚本命令,大家访问上面的网址就可以看到,这里我简单介绍下项目的结构:





├── Dockerfile        //创建镜像的Dockerfile
├── pkg //编译好的二进制包,可以直接使用,此外软件运行的一些配置文件或第三方包也放在这里
│ ├── conf
│ │ ├── fastcgi.conf
│ │ ├── http.d
│ │ ├── include
│ │ ├── koi-utf
│ │ ├── koi-win
│ │ ├── logrotate.conf
│ │ ├── logrotate.d
│ │ ├── mime.types
│ │ ├── Nginx.conf
│ │ ├── scgi_params
│ │ ├── uwsgi_params
│ │ └── win-utf
│ ├── luajit-2.0.3.tar.gz
│ └── Nginx-1.8.1.tar.gz
├── README.md
├── script //里面放构建脚本
│ ├── build_image.sh //构建镜像使用
│ ├── build_pkg.sh //编译软件包时使用
│ ├── init.sh //容器启动时执行
│ └── pre_build.sh //软件依赖的共享库,编译和构建时都会用到
└── src //编译时需要的软件源码
├── modules
│ ├── ngx_devel_kit-0.2.19.tgz
│ ├── ngx_echo-0.53.tgz
│ └── ngx_lua-0.9.7.tgz
├── Nginx-1.8.1.tar.gz
└── openssl-1.0.2h.tar.gz




DockerFile说明



Dockerfile结构如下:





FROM andals/CentOS:7
MAINTAINER ligang

LABEL name="Nginx Image"
LABEL vendor="Andals"

COPY pkg/ /build/pkg/
COPY script/ /build/script/

RUN /build/script/build_image.sh

CMD /build/script/init.sh


整个构建框架为:




  1. 把构建需要的包(PKG目录中)放到镜像中

  2. 把构建脚本放到镜像中

  3. 执行构建脚本

  4. 容器启动时,执行init.sh,里面启动相应的服务



Readme.md中记录了执行构建的命令和容器运行命令,示例运行如下:





ligang@vm-xUbuntu16 ~/devspace/dbuild $ git clone git@gitee.com:andals/docker-Nginx.git Nginx
Cloning into 'Nginx'...
......

ligang@vm-xUbuntu16 ~/devspace/dbuild $ cd Nginx/
ligang@vm-xUbuntu16 ~/devspace/dbuild/Nginx $ ngxVer=1.8.1
ligang@vm-xUbuntu16 ~/devspace/dbuild/Nginx $ docker build -t andals/Nginx:${ngxVer} ./
Sending build context to Docker daemon 30.7MB
Step 1/8 : FROM andals/CentOS:7
......
Successfully built ea8147743031
Successfully tagged andals/Nginx:1.8.1

ligang@vm-xUbuntu16 ~/devspace/dbuild/Nginx $ docker run -d --name=Nginx-${ngxVer} --volumes-from=data-home -v /data/Nginx:/data/Nginx --net=host andals/Nginx:${ngxVer}
dbf3c0617eb34c4b1b4ea54c2961989612d5474db3b1acd1d717221e6e5cb516


说明: –volumes-from=data-home这个就是我放置代码的数据卷,我喜欢把代码放到$HOME下面,所以这个卷的相关信息如下:





ligang@vm-xUbuntu16 ~ $ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
578912a08ea7 andals/CentOS:7 "echo Data Volumn Ho…" 9 days ago Exited (0) 9 days ago data-home
......

ligang@vm-xUbuntu16 ~ $ docker inspect 578912a08ea7
......
"Mounts": [
{
"Type": "bind",
"Source": "/home",
"Destination": "/home",
"Mode": "",
"RW": true,
"Propagation": "rprivate"
}
......


/data/Nginx中放置Nginx的conf、log等,每个软件运行时的conf、log、data等我都统一放置在/data下面,如:





ligang@vm-xUbuntu16 ~ $ tree -d /data/ -L 1
/data/
├── mariadb
├── Nginx
└── redis

ligang@vm-xUbuntu16 ~ $ tree -d /data/Nginx/
/data/Nginx/
├── conf
│ ├── http.d
│ ├── include
│ └── logrotate.d
└── logs


启动容器时使用–net=host,作为开发环境简单实用 我就是通过这种方法完成了开发环境的构建,不再有多余的重复工作,并且新机器部署开发环境效率极高。



我目前用到的容器环境如下:





ligang@vm-xUbuntu16 ~ $ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
dbf3c0617eb3 andals/Nginx:1.8.1 "/bin/sh -c /build/s…" 33 minutes ago Up 33 minutes Nginx-1.8.1
3e31ef433298 andals/php:7.1.9 "/bin/sh -c /build/s…" 8 hours ago Up 8 hours php-7.1.9
360f94bf9c43 andals/jekyll:latest "/bin/sh -c /build/s…" 9 days ago Up 10 hours jekyll-latest
0a7d58d1ca5e andals/redis:4.0.8 "/bin/sh -c /build/s…" 9 days ago Up 10 hours redis-4.0.8
fdaa655b4a11 andals/samba:4.4.16 "/bin/sh -c /build/s…" 9 days ago Up 10 hours samba-4.4.16
6ad00a69befd andals/mariadb:10.2.14 "/bin/sh -c /build/s…" 9 days ago Up 10 hours mariadb-10.2.14
578912a08ea7 andals/CentOS:7 "echo Data Volumn Ho…" 9 days ago Exited (0) 9 days ago data-home




辅助工具dockerbox



使用Docker环境后,有个问题,就是没有办法很方便的和软件交互。 这是因为软件都执行在容器中,比如重启Nginx吧,需要下面这几步:




  1. 找到Nginx这个容器

  2. 进入Nginx这个容器

  3. 在容器里面再执行reload



也可以是:




  1. 找到Nginx这个容器

  2. 使用docker exec



但无论哪种方式,都比原先直接执行命令麻烦的多。



另外,有时也需要进入容器中,查看服务的运行情况。



为了方便的做这些事情,我开发了一个工具dockerbox,可以很方便的做到这些事情。



dockerbox的详情及使用方法请见:https://github.com/ligang1109/dockerbox





配置开机运行



最后再说下如何配置开机启动。 我使用虚拟机搭建的开发环境,所以配置这个会省事好多,我使用用了systemd:





ligang@vm-xUbuntu16 ~ $ ll /lib/systemd/system/dstart.service
-rw-r--r-- 1 root root 229 43 21:35 /lib/systemd/system/dstart.service

ligang@vm-xUbuntu16 ~ $ cat /lib/systemd/system/dstart.service
[Unit]
Description=Docker Container Starter
After=network.target docker.service
Requires=docker.service

[Service]
ExecStart=/usr/local/bin/dbox -dconfPath=/home/ligang/.dconf.json start all

[Install]
WantedBy=multi-user.target


dbox请参考dockerbox的使用方法。





结束语



上面说的是我现在使用的开发环境搭建方法,有兴趣爱折腾的同学不妨试试看,如果你有更好的方法,也希望能分享给我。



原文链接:使用Docker高效搭建开发环境

什么是服务网格?

grace_shi 发表了文章 • 0 个评论 • 292 次浏览 • 2019-06-04 22:29 • 来自相关话题

服务网格 是一个可配置的低延迟的基础设施层,目的是通过API(应用程序编程接口)处理应用程序服务之间的大量基于网络的进程间通信。服务网络确保容器化的短暂存在的应用程序的基础结构服务之间的通信快速,可靠和安全。网格提供关键功能,包括服务发现,负载平衡,加密,可观 ...查看全部
服务网格 是一个可配置的低延迟的基础设施层,目的是通过API(应用程序编程接口)处理应用程序服务之间的大量基于网络的进程间通信。服务网络确保容器化的短暂存在的应用程序的基础结构服务之间的通信快速,可靠和安全。网格提供关键功能,包括服务发现,负载平衡,加密,可观察性,可追溯性,身份验证和授权,以及对断路器模式【1】的支持。

服务网格是如何实现的呢?它通常会为每个服务实例提供一个称为边车(sidecar)的代理实例。这些边车会处理服务间的通信,监控和安全相关的问题, 以及任何可以从各个服务中抽象出来的东西。这样,开发人员就可以专注于服务中应用程序代码的开发,支持和维护,而运维团队可以负责维护服务网格以及运行应用程序。

Istio,由Google,IBM和Lyft支持,是目前最著名的服务网格架构。Kubernetes,最初由Google设计,是目前Istio支持的唯一容器编排框架。供应商正在寻求商业版本的Istio。如果Istio能添加到开源项目中,将会带来更大的价值。

Istio并不是唯一的选择,其他服务网格实现也在开发中。目前边车代理模式是最受欢迎的,例如Buoyant,HashiCorp,Solo.io等项目都使用了这种模式。与此同时,Netflix的技术套件使用了一种替代架构,他们使用了应用程序库(包含Ribbon,Hysterix,Eureka,Archaius)来提供服务网格功能,而Azure Service Fabric等平台在应用程序框架中嵌入了类似服务网格的功能。 服务网格包含一些专业术语来描述组件服务和功能:

  • 容器编排框架。随着越来越多的容器被添加到应用程序的基础架构中,用于监视和管理容器组的容器编排框架变得至关重要。
  • 服务和实例(Kubernetes Pod)。实例是微服务的单个运行副本。有时实例是一个容器;在Kubernetes中,一个实例由一小组相互依赖的容器(称为Pod)组成。客户端很少直接访问实例或Pod,通常他们会访问服务,服务通常是一组可扩展且具有容错性的实例或Pod(副本)。
  • 边车代理。 边车代理与单个实例或Pod一起运行。 边车代理的目的是路由或者代理从容器发出或者接收的流量。 边车与其他边车代理进行通信,编排框架会管理这些边车。许多服务网格的实现会使用边车代理来拦截和管理实例或Pod的所有进出流量。
  • 服务发现。当实例需要与不同的服务进行交互时,它需要找到 - 发现 - 其他服务的健康的,可用的实例。通常,这个实例会执行DNS查找来寻找其他服务的实例。容器编排框架保留实例列表(这些实例都可以正常接收请求),并且框架会提供DNS查询的接口。
  • 负载均衡。大多数编排框架已经提供了第4层(传输层)的负载均衡。服务网络实现了更复杂的第7层(应用层)负载均衡,具有更丰富的算法以及更强大的流量管理。同时可以通过API修改负载均衡的参数,从而可以编排蓝绿部署【2】或金丝雀部署【3】。
  • 加密。服务网格可以加密和解密请求和响应,因此每个服务不需要额外对请求进行加密,减少了负担。服务网格还可以通过优先重用现有的持久连接来提高性能,这减少新连接的创建(创建新连接十分耗费计算资源)。一般实现加密流量都是用双向TLS(mTLS),其中公钥架构(PKI,public key infrastructure)生成并分发证书和密钥,提供给边车代理使用。
  • 身份验证和授权。服务网格可以授权和验证从应用程序外部和内部发出的请求,仅向实例发送经过验证的请求。
  • 支持断路器模式【1】。服务网格可以支持断路器模式,这可以隔离不健康的实例,然后在安全的情况下逐渐将它们恢复并加入到健康的实例池中。

服务网格应用中管理实例之间的网络流量的的部分称为数据平面。另外有一个独立的控制平面负责生成和部署数据平面的配置(这个配置可以控制数据平面的行为)。控制平面通常包含(或被设计为连接到)一个API,命令行界面和用于管理App的图形用户界面。

*服务网格中的控制平面在数据平面中边车代理上分发配置*

服务网格架构的一个常见用例是在使用容器和微服务时解决非常苛刻的操作问题。微服务领域的先驱包括Lyft,Netflix和Twitter等公司,这些公司任何时间都能为全球数百万用户提供强大的服务。 (请参阅我们对Netflix面临的一些架构挑战的深入描述。)如果应用程序对这方面要求比较低,那么更简单的架构就足够了。

服务网格架构不可能解决所有应用程序操作和交付问题。架构师和开发人员可以选择多种工具,这些工具之中,有些是锤子,可以解决不同类型的问题,而另一些可能是钉子。例如,NGINX微服务参考架构包括几种不同的模型,这些模型提供了使用微服务来解决问题的一系列方法。 服务网格架构中的组成部分 - 例如:NGINX,容器,Kubernetes,以及微服务(作为架构方法),可以在非服务网格架构实现中高效地使用。例如,Istio是作为一个完整的服务网格架构开发的,但其模块化的设计意味着开发人员可以自由选择他们需要的部分组件技术。考虑到这一点,即使您不确定是否以及何时会完全实现服务网格应用程序,也值得深入了解一下服务网格概念。 

注释: 

【1】断路器模式: 一种设计模式。用以侦测错误,并避免不断地触发相同的错误(如维护时服务不可用、暂时性的系统问题或是未知的系统错误)。https://zh.wikipedia.org/wiki/斷路器設計模式 
【2】蓝绿部署(blue‑green deployment):蓝绿部署是保留老版本,同时部署新版本然后进行测试,测试通过后,将流量切换到新版本,然后老版本同时也升级到新版本。 
【3】金丝雀部署(canary deployment):又叫做灰度部署,即选择部分部署新版本,将部分流量引入到新版本,新老版本同时提供服务。等待灰度的版本测试完毕,之后全量覆盖老版本。 

原文链接:What Is a Service Mesh? 

============================================================================== 
译者介绍:Grace,程序员,研究生毕业于SUNY at Stony Brook,目前供职于Linktime Cloud Company,对大数据技术以及数据可视化技术感兴趣。

Kubernetes IN Docker - local clusters for testing Kubernetes

老马 发表了文章 • 0 个评论 • 289 次浏览 • 2019-06-04 21:52 • 来自相关话题

Brief Kind(Kubernetes IN Docker是一个用来快速创建 ...查看全部

Brief



Kind(Kubernetes IN Docker是一个用来快速创建和测试kubernetes的工具,Kind把环境的依赖降低到了最小,仅需要机器安装了Docker即可。



Kind 可以做什么?




  • 快速创建一个或多个Kubernetes集群(几分钟)

  • 支持HA Master部署高可用的Kubernetes集群

  • 支持从源码构建并部署一个Kubernetes集群

  • 可以快速低成本体验一个最新的Kubernetes集群,并支持Kubernetes的绝大部分功能

  • 支持本地离线运行一个多节点集群



Kind 有哪些优势?




  • 最小的安装依赖,仅需要安装Docker即可

  • 使用快速简单,使用kind cli工具即可快速创建集群

  • 使用container来 mockkubernetes node

  • 内部使用kubeadm的官方主流部署工具

  • 使用了containerd

  • 通过了CNCF官方的k8s conformance测试





Usage





GO111MODULE="on" go get sigs.k8s.io/kind@v0.3.0 && kind create cluster




How it work



Kind 使用一个 container 来模拟一个 Node,在 Container 里面跑了 systemd ,并用 systemd 托管了 kubelet 以及 containerd,然后容器内部的 kubelet 把其他 Kubernetes 组件,比如 kube-apiserver、etcd、CNI 等组件跑起来。



可以通过配置文件的方式,来通过创建多个 container 的方式,来模拟创建多个 Node,并以这些 Node 来构建一个多节点的 Kubernetes 集群。



Kind 内部使用了 kubeadm 这个工具来做集群的部署,包括 ha master 的高可用集群,也是借助 kubeadm 提供的aplha特性提供的。同时,在 HA Master 下,额外部署了一个 Nginx 用来提供负载均衡 vip。





Build Images



Kind 的镜像分为两个,一个 Node 镜像,一个 Base 镜像。



Node 镜像



Node 镜像的构建比较复杂,目前是通过运行 Base 镜像,并在 Base 镜像内执行操作,再保存此容器内容为镜像的方式来构建的,包含的操作有:




  • 构建 Kubernetes 相关资源(比如二进制文件和镜像)

  • 运行一个用于构建的容器

  • 把构建的 Kubernetes 相关资源复制到容器里

  • 调整部分组件配置参数,以支持在容器内运行

  • 预先拉去运行环境需要的镜像

  • 通过 docker commit 方式保存当前的构建容器为 node 镜像



具体的逻辑,可以参考node.go



Base 镜像



Base 镜像目前使用了 Ubuntu 19.04 作为基础镜像,做了下面的调整:




  • 安装 systemd 相关的包,并调整一些配置以适应在容器内运行

  • 安装 Kubernetes 运行时的依赖包,比如 conntrack、socat、CNI

  • 安装容器



运行环境,比如 containerd、crictl




  • 配置自己的 ENTRYPOINT 脚本,以适应和调整容器内运行的问题



具体的逻辑,可以参考构建的Dockerfile





Create Cluster



Kind 创建集群的基本过程为:




  1. 根据传入的参数,来创建 container,分为 control node 和 worker node 两种(如果是 ha master,还有一个 loadbalancer node)

  2. 如果需要,配置 loadbalancer 的配置,主要是 Nginx 配置文件

  3. 生成 kubeadm 配置

  4. 对于第一个控制节点,使用 kubeadm init 初始化单节点集群

  5. 配置安装 CNI 插件

  6. 配置存储(实际是安装了一个使用 hostpath 的 storageclass)

  7. 其他的控制节点,通过kubeadm join --experimental-control-plane的方式来扩容控制节点

  8. 通过 kubeadm join 扩容其他的工作节点

  9. 等待集群创建完成

  10. 生成访问配置,打印使用帮助具体的创建流程,可以参考代码create.go



这里关于每个容器,是如何作为 Node 跑起来的,可以简单讲解些原理:



根据不同的角色,调用不同的函数创建节点nodes.go





// TODO(bentheelder): remove network in favor of []cri.PortMapping when that is in
func (d *nodeSpec) Create(clusterLabel string) (node *nodes.Node, err error) {
// create the node into a container (docker run, but it is paused, see createNode)
// TODO(bentheelder): decouple from config objects further
switch d.Role {
case constants.ExternalLoadBalancerNodeRoleValue:
node, err = nodes.CreateExternalLoadBalancerNode(d.Name, d.Image, clusterLabel, d.APIServerAddress, d.APIServerPort)
case constants.ControlPlaneNodeRoleValue:
node, err = nodes.CreateControlPlaneNode(d.Name, d.Image, clusterLabel, d.APIServerAddress, d.APIServerPort, d.ExtraMounts)
case constants.WorkerNodeRoleValue:
node, err = nodes.CreateWorkerNode(d.Name, d.Image, clusterLabel, d.ExtraMounts)
default:
return nil, errors.Errorf("unknown node role: %s", d.Role)
}
return node, err
}


节点(容器)创建时,通过配置 –privileged,挂载 tmpfs,修改主机名等,来运行节点create



func createNode(name, image, clusterLabel, role string, mounts []cri.Mount, extraArgs ...string) (handle *Node, err error) {
runArgs := []string{
"-d", // run the container detached
"-t", // allocate a tty for entrypoint logs
// running containers in a container requires privileged
// NOTE: we could try to replicate this with --cap-add, and use less
// privileges, but this flag also changes some mounts that are necessary
// including some ones docker would otherwise do by default.
// for now this is what we want. in the future we may revisit this.
"--privileged",
"--security-opt", "seccomp=unconfined", // also ignore seccomp
"--tmpfs", "/tmp", // various things depend on working /tmp
"--tmpfs", "/run", // systemd wants a writable /run
// some k8s things want /lib/modules
"-v", "/lib/modules:/lib/modules:ro",
"--hostname", name, // make hostname match container name
"--name", name, // ... and set the container name
// label the node with the cluster ID
"--label", clusterLabel,
// label the node with the role ID
"--label", fmt.Sprintf("%s=%s", constants.NodeRoleKey, role),
}

// pass proxy environment variables to be used by node's docker deamon
proxyDetails := getProxyDetails()
for key, val := range proxyDetails.Envs {
runArgs = append(runArgs, "-e", fmt.Sprintf("%s=%s", key, val))
}

// adds node specific args
runArgs = append(runArgs, extraArgs...)

if docker.UsernsRemap() {
// We need this argument in order to make this command work
// in systems that have userns-remap enabled on the docker daemon
runArgs = append(runArgs, "--userns=host")
}

err = docker.Run(
image,
docker.WithRunArgs(runArgs...),
docker.WithMounts(mounts),
)

// we should return a handle so the caller can clean it up
handle = FromName(name)
if err != nil {
return handle, errors.Wrap(err, "docker run error")
}

return handle, nil
}


More



Kind是一个比较简单有趣的项目,Kind的scope定的比较明确和具体,也定的比较小,其实借助 Kind 或者 Kind 的思想,可以做更多的事情,比如:




  • 在单节点部署自己的上层平台

  • 借助容器 mock 节点的方式,优化现有的测试方案

  • 自动化的部署测试

  • 自动化的 e2e 测试



原文链接:https://mp.weixin.qq.com/s/Vhr0ml1wI1BIoxKqKGTXRg

58集团云平台架构实践与演进

玻璃樽 发表了文章 • 0 个评论 • 458 次浏览 • 2019-06-04 19:23 • 来自相关话题

【编者的话】在17年底,我们分享了《高可用Docker容器云在58集团的实践》这篇文章,对整个容器云基础环境搭建与应用选型进行了详细介绍,本文是在该文章基础之上的进阶篇,是针对具体业务场景的落地解决方案。如果对基础环境选型比较感兴趣,可以查看上篇文章,在本文的 ...查看全部
【编者的话】在17年底,我们分享了《高可用Docker容器云在58集团的实践》这篇文章,对整个容器云基础环境搭建与应用选型进行了详细介绍,本文是在该文章基础之上的进阶篇,是针对具体业务场景的落地解决方案。如果对基础环境选型比较感兴趣,可以查看上篇文章,在本文的最后会附上相关文章的链接。对于上篇文章讨论过的内容,本文将不再进行详细讨论。后续每个月,云团队都会选择平台中某一具体领域的相关工作进行详细讨论与分享,欢迎大家关注。大家想了解哪方面的实现方案与细节,可进行相应留言。
#背景

通过容器化技术,58云计算平台主要解决以下几个问题:

  1. 资源利用率低:通过云化技术可以将资源利用率提升至原有的3-4倍,甚至更高。
  2. 服务扩容效率低:将传统扩容的时间从小时级别降低为分钟级别。
  3. 上线流程不规范:基于同一的镜像模板,约束整个上线过程。

为了解决上述问题,云团队通过技术选型与反复论证最终决定基于Docker与Kubernetes体系构建整个容器云环境。

云计算平台的发展历程如下:
1.png

#整体架构

58云计算平台的整体架构如下:
2.png

所有容器云的架构都是相似的,这里不做赘述,具体可查看上篇文章。

云计算平台承载了集团90%以上的业务流量,作为核心的服务管理与上线系统,它并不是独立运作的,为了保证整个上线流程的一致性与流畅度,满足业务日常管理与维护的通用需求,云平台与集团内部多个核心系统与组件进行了相应的对接与联动。
3.jpg

在项目管理方面,云平台与代码管理系统、项目管理等系统进行了内部对接,实现了代码从编译到生成镜像,再到环境部署的完全自动化。

在运维方面,云平台与CMDB、服务树、监控系统等多个运维系统进行了打通与整合,保证整个运维体系的完整性与用户体验的一致性。

在基础组件方面,集团内部的服务治理体系与常用的中间件系统都针对云平台进行了相应的改造,以适配云平台的工作模式,同时云平台也集成了现有的数据收集组件,保证数据流与用户习惯在云化前后是一致的。

云平台的使用方面,平台定义了四套环境,四套环境基于唯一的镜像仓库,这使得同一个代码版本可以在不同的环境之间进行流转,在保证代码唯一性的同时也使得相关环境的变更不会传递到下一个环境。
4.png

#架构演进

构建一个适配多种业务场景的容器云有很多细节需要考虑,云团队做了很多工作,由于篇幅关系,这里主要和大家分享58云计算平台在“网络架构”和“服务发现”两个核心组件上的架构实践与演进。
##网络架构

在网络架构方面,通过对比常用的六种容器组网模型,云计算平台选择“bridge+vlan”的方式作为基础网络模型。
5.png

原生bridge网络模型存在两个明显的缺点:IP利用率低、缺少网络限速。

为了解决IP地址利用率低的问题,云平台基于docker的CNM接口开发了网络插件,支持多个宿主间共享同一个容器网段,也支持IP地址在不同的宿主间复用。
6.png

这种网络模式下,分配给业务实例的IP地址是随机的,实例每次重启IP地址都可能会发生变更。在推进业务云化的早期,这种模式被业务所接受,但是随着云化进程的不断深入,业务方面提出了更高的要求:固定IP。

业务需求来源于真实的案例:有些服务要求IP不能发生变化,特别是某些依赖于第三方外部接口的服务。同时集团内部很多系统都是以IP固定为前提,如果IP随机分配,这些现有系统将不可用或很难用,极大的影响用户体验。如果IP固定的需求不能被满足,业务云化将很难推进下去。所以在18年4月份,云平台对网络架构进行了升级,支持了固定IP模式。网络架构的升级很好的保证了业务云化的进程。
7.png

固定IP的网络架构基于Kubernetes的CNI接口实现,增加了IP控制器模块,业务每次扩缩容时,都会与IP控制器交互,进行IP的变更。在业务正常升级流程中,归属于业务的IP将不会发生变化。依托于腾讯机房的网络支撑,平台将容器网段的路由规则下发到交换机,实现了容器的全网漂移,而不仅仅局限于固定的交换机。如果你想和更多Kubernetes技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态

针对网络限速的需求,云平台结合容器网络虚拟化的特性,基于自研监控与tc工具实现了容器网络限速的能力。
8.jpg

标准的tc工具只支持单向限速,即出口流量限速。单方向限速无法满足业务的实际需求。在深入研究容器网络虚拟化的原理后,我们发现,容器在做虚拟化时,会创建一个网卡对,他们是对等的网络设备,在宿主机上体现为veth,在容器内部体现为eth0。基于这个特性,我们实现了双向限速:即对两块网卡同时做出口流量限速。由于是对等网卡,所以对veth做出口流量限速就相当于对eth0做入口流量限速。

在网络限速的基础上,云平台又进行了多维度的完善,分别支持动态限速、秒级限速与弹性限速等网络应用场景,极大的实现带宽复用。
##服务发现

服务发现是容器云平台中非常核心的服务组件,是流量的入口,也是服务对外暴露的接口。云平台中IP动态分配,节点弹性伸缩,需要有一套自动变更负载均衡器的方式。对于后端服务,集团有成熟的服务治理框架与体系,在云化过程中,中间件团队对服务治理体系进行了改造使其可以适配不断变化的云环境。对于前端服务,集团是基于Nginx做负载均衡,并且没有一套支持IP自动变更的架构。为了实现前端服务的云化,云平台团队与运维团队共同设计了全新的服务发现架构。

在调研阶段,云团队也调研了Kubernetes自带的服务发现机制,这一机制无法提供复杂的负载均衡策略,并且无法满足业务在特殊场景下需要对部分节点摘除流量的需求,所以最终我们否定了这一方案。
9.png

这是业界典型的服务发现架构。服务端和负载均衡器通过Consul进行解耦。服务注册在Consul中,负载均衡器通过Watch Consul中目录的变化来实时感知节点变更。在云化的早期,为了满足快速上线的需求,云平台对集团内部的Java Web框架进行了修改,使得其以心跳的方式自动注册到Consul中。这很好的解决了负载均衡器自动感知业务变更的问题以及云化过程中的流量灰度问题。

这一架构也引入了新的问题:调试问题与多语言扩展问题。由于是Java框架代理业务注册到Consul中,这使得活跃节点无法被下掉流量从而进行调试,但实际场景中很多故障调试需要下掉流量后才能进行。对Java语言的支持是通过修改框架来完成的,而集团中还有很多其他类型语言,比如PHP、Node.js和Go等。对每种接入语言或框架都需要以修改代码的方式才能接入到云平台中来,开发成本高并且对业务不友好。Consul被直接暴露给服务方,也增加了Consul的安全风险。

基于此,在18年4月,云平台对整个服务发现架构进行了升级。
10.png

新的服务发现架构中,业务与Consul中间增加了Proxy代理层,代理层通过Watch Kubernetes事件实时感知业务节点信息的变化,配合健康检查功能可以保证业务节点在变更时流量无损。基于代理层,任意语言的程序不需要做任何修改,仅通过简单配置即可接入到云平台。服务的注册与发现托管到云平台,Consul组件对业务透明,开发人员使用更友好。
#复盘与反思

回顾这两年来的容器云架构演进过程与业务云化历程,复盘遇到的棘手问题及其解决方案,与大家共同探讨。

在整个云平台的设计之初,我们有如下的设计考量:
11.png

容器云都会面临一个问题:对于业务来说,是容器还是虚拟机?虚拟机是业务习惯的使用模式,业务更容易接受。容器是以服务为核心,服务存在即存在,服务关闭即销毁,不再是虚拟机以机器为核心的模式。思虑再三,最终决定以容器的模式来定位,全新的平台提供全新的服务模式与体验。

虽然是以容器为核心,但容器中可运行单进程也可运行多进程。我们规范容器中只运行一个业务进程,防止由于运行多进程而导致业务之间相互干扰情况的发生,这类问题很难进行排查与定位。

去Agent化:在传统物理机模式下,一台物理机上可能会有多个Agent:运维管控Agent、监控Agent、业务自定义Agent等。云化后面临一个现实的问题:我们需要在每个容器中都安装这些Agent么?如果每个容器中都集成这些Agent,对云平台来说是简单的,这没有任何工作量。但是一台物理机上可能会运行上百个容器,这些Agent数量也会大量膨胀,带来系统负载的增加的问题。基于此,云平台投入大量的精力来实现容器的无Agent化,即在所有功能正常运行的前提下,Agent只运行在宿主上,不会运行在容器中。

业务云化过程中云团队遇到了很多问题案例,并形成自己独特的解决方案。这里选择了几个有代表性的案例,与大家进行分享。
12.png

服务启动耗CPU过高是云化早期时遇到的棘手问题。这个问题产生的原因很多:Java的语言特性导致JVM是在运行中逐步进行优化的;内部的很多开发框架都是在流量过来时才初始化链接;某些业务资源也是在流量过来时才进行初始化。当流量分发过来时,多种资源同时初始化导致服务需要大量的CPU资源,超过了平台为其分配的CPU配额,服务出现大量超时与抛弃。这一问题的简单解法是多分配CPU资源,但从长远角度来看这会导致资源利用无法有效把控,也会影响弹性调度的效果。最终,我们从两个维度解决这一问题:在调用方增加预热策略,流量逐步分发过来,CPU资源使用更平滑;在服务方增加预热方法,默认初始化链接等资源,同时引导用户进行自定义的初始化。

在容器监控维度方面,由于默认的监控数据是基于随机采样的分钟级数据,导致当服务出现短期秒级的CPU波动时,监控系统无法捕获,从而影响问题的排查与定位。针对这一问题,云平台对监控维度进行了深化,增加了容器级别秒级监控,实时采集每分钟的最大值上报,便于问题的排查与跟踪。
13.png

由于Cgroups只对CPU和内存资源进行了隔离与限速,并没有对系统负载进行隔离,如果容器中运行进程或线程数过多,会导致宿主机整体的负载波动,进而对上面的所有服务都会造成影响。

云平台增加了两个维度的控制策略:容器级别的最大线程数控制,防止由于单个容器线程数过多会对宿主造成影响。宿主级别的过载保护,当宿主上的负载过高时,过载保护策略会自动触发宿主上的容器进行漂移,直至负载降至合理的范围。

云平台必须关闭swap交换分区,这是一个深刻的经验教训。在云化的早期,云平台的交换分区没有关闭,部分服务经常出现由于使用交换分区而导致的耗时随机抖动,这类问题很难排查,耗费了大量的精力。

58云计算平台核心软件版本变迁过程如下:
14.png

#后记

容器云在58集团的实践与探索过程中,云团队做了很多技术选型与优化工作,也进行了很多技术方案的架构演进工作,究其根本原因是58集团是发展了十多年的互联网公司,它有自己独特的业务场景和成熟的开发体系与基础组件,容器云在集团内部起步较晚,内部系统很难为云平台做适配,只能云平台通过不断演进来适配不停发展的业务场景。这也是在较有规模的互联网公司做基础架构与创业公司的区别和挑战所在。所幸在团队的共同努力下,我们用1年多的时间完成了集团流量服务的云化工作。目前,集团内部还有很多服务需要云化,未来的挑战更大。

原文链接:https://mp.weixin.qq.com/s/gJM5-DByvMH54QhVsJ5HEA

详解Eureka缓存机制

阿娇 发表了文章 • 0 个评论 • 275 次浏览 • 2019-06-04 14:45 • 来自相关话题

【编者的话】Eureka是Netflix开源的、用于实现服务注册和发现的服务。Spring Cloud Eureka基于Eureka进行二次封装,增加了更人性化的UI,使用更为方便。但是由于Eureka本身存在较多缓存,服务状态更新滞后,最常见的状况是:服务下 ...查看全部
【编者的话】Eureka是Netflix开源的、用于实现服务注册和发现的服务。Spring Cloud Eureka基于Eureka进行二次封装,增加了更人性化的UI,使用更为方便。但是由于Eureka本身存在较多缓存,服务状态更新滞后,最常见的状况是:服务下线后状态没有及时更新,服务消费者调用到已下线的服务导致请求失败。本文基于Spring Cloud Eureka 1.4.4.RELEASE,在默认region和zone的前提下,介绍Eureka的缓存机制。
#一、AP特性

从CAP理论看,Eureka是一个AP系统,优先保证可用性(A)和分区容错性(P),不保证强一致性(C),只保证最终一致性,因此在架构中设计了较多缓存。
1.jpg

Eureka高可用架构
#二、服务状态

Eureka服务状态enum类:com.netflix.appinfo.InstanceInfo.InstanceStatus
2.png

#三、Eureka Server

在Eureka高可用架构中,Eureka Server也可以作为Client向其他server注册,多节点相互注册组成Eureka集群,集群间相互视为peer。Eureka Client向Server注册、续约、更新状态时,接受节点更新自己的服务注册信息后,逐个同步至其他peer节点。如果你想和更多Spring Cloud技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态

【注意】如果server-A向server-B节点单向注册,则server-A视server-B为peer节点,server-A接受的数据会同步给server-B,但server-B接受的数据不会同步给server-A。
##缓存机制

Eureka Server存在三个变量:(registry、readWriteCacheMap、readOnlyCacheMap)保存服务注册信息,默认情况下定时任务每30s将readWriteCacheMap同步至readOnlyCacheMap,每60s清理超过90s未续约的节点,Eureka Client每30s从readOnlyCacheMap更新服务注册信息,而UI则从registry更新服务注册信息。
3.jpg

三级缓存:
4.jpg

缓存相关配置:
5.jpg

关键类:
6.jpg

#四、Eureka Client

Eureka Client存在两种角色:服务提供者和服务消费者,作为服务消费者一般配合Ribbon或Feign(Feign内部使用Ribbon)使用。Eureka Client启动后,作为服务提供者立即向Server注册,默认情况下每30s续约(renew);作为服务消费者立即向Server全量更新服务注册信息,默认情况下每30s增量更新服务注册信息;Ribbon延时1s向Client获取使用的服务注册信息,默认每30s更新使用的服务注册信息,只保存状态为UP的服务。

二级缓存:
7.jpg

缓存相关配置:
8.jpg

关键类:
9.jpg

#五、默认配置下服务消费者最长感知时间

10.jpg

考虑如下情况:

* 0s时服务未通知Eureka Client直接下线;
* 29s时第一次过期检查evict未超过90s;
* 89s时第二次过期检查evict未超过90s;
* 149s时第三次过期检查evict未续约时间超过了90s,故将该服务实例从registry和readWriteCacheMap中删除;
* 179s时定时任务从readWriteCacheMap更新至readOnlyCacheMap;
* 209s时Eureka Client从Eureka Server的readOnlyCacheMap更新;
* 239s时Ribbon从Eureka Client更新。

因此,极限情况下服务消费者最长感知时间将无限趋近240s。
11.jpg

#六、应对措施

服务注册中心在选择使用Eureka时说明已经接受了其优先保证可用性(A)和分区容错性(P)、不保证强一致性(C)的特点。如果需要优先保证强一致性(C),则应该考虑使用ZooKeeper等CP系统作为服务注册中心。分布式系统中一般配置多节点,单个节点服务上线的状态更新滞后并没有什么影响,这里主要考虑服务下线后状态更新滞后的应对措施。
##Eureka Server

1、缩短readOnlyCacheMap更新周期。缩短该定时任务周期可减少滞后时间。
eureka.server.responsecCacheUpdateIntervalMs: 10000  # Eureka Server readOnlyCacheMap更新周期

2、关闭readOnlyCacheMap。中小型系统可以考虑该方案,Eureka Client直接从readWriteCacheMap更新服务注册信息。
eureka.server.useReadOnlyResponseCache: false        # 是否使用readOnlyCacheMap

##Eureka Client

1、服务消费者使用容错机制。如Spring Cloud Retry和Hystrix,Ribbon、Feign、Zuul都可以配置Retry,服务消费者访问某个已下线节点时一般报ConnectTimeout,这时可以通过Retry机制重试下一个节点。

2、服务消费者缩短更新周期。Eureka Client和Ribbon二级缓存影响状态更新,缩短这两个定时任务周期可减少滞后时间,例如配置:
eureka.client.registryFetchIntervalSeconds: 5        # Eureka Client更新周期
ribbon.ServerListRefreshInterval: 2000 # Ribbon更新周期

3、服务提供者保证服务正常下线。服务下线时使用kill或kill -15命令,避免使用kill -9命令,kill或kill -15命令杀死进程时将触发Eureka Client的shutdown()方法,主动删除Server的registry和readWriteCacheMap中的注册信息,不必依赖Server的evict清除。

4、服务提供者延迟下线。服务下线之前先调用接口使Eureka Server中保存的服务状态为DOWN或OUT_OF_SERVICE后再下线,二者时间差根据缓存机制和配置决定,比如默认情况下调用接口后延迟90s再下线服务即可保证服务消费者不会调用已下线服务实例。
#七、网关实现服务下线实时感知

在软件工程中,没有一个问题是中间层解决不了的,而网关是服务提供者和服务消费者的中间层。以Spring Cloud Zuul网关为例,网关作为Eureka Client保存了服务注册信息,服务消费者通过网关将请求转发给服务提供者,只需要做到服务提供者下线时通知网关在自己保存的服务列表中使该服务失效。为了保持网关的独立性,可实现一个独立服务接收下线通知并协调网关集群。下篇文章将详细介绍网关如何实现服务下线实时感知,敬请期待!

原文链接:https://mp.weixin.qq.com/s/zwoIDzX8WouYVrBfMJMpJQ