使用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 个评论 • 293 次浏览 • 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

API网关如何实现对服务下线实时感知

翔宇 发表了文章 • 0 个评论 • 244 次浏览 • 2019-06-04 15:50 • 来自相关话题

上篇文章《Eureka 缓存机制》介绍了Eureka的缓存机制,相信大家对Eureka有了进一步的了解,本文将详细介绍API网关如何实现服务下线的实时感知。 #一、前言 在基于云的微服务应用中,服务实例的网络位置都是动态分配的。而且 ...查看全部
上篇文章《Eureka 缓存机制》介绍了Eureka的缓存机制,相信大家对Eureka有了进一步的了解,本文将详细介绍API网关如何实现服务下线的实时感知。
#一、前言

在基于云的微服务应用中,服务实例的网络位置都是动态分配的。而且由于自动伸缩、故障和升级,服务实例会经常动态改变。因此,客户端代码需要使用更加复杂的服务发现机制。

目前服务发现主要有两种模式:客户端发现和服务端发现。

* 服务端发现:客户端通过负载均衡器向服务注册中心发起请求,负载均衡器查询服务注册中心,将每个请求路由到可用的服务实例上。
* 客户端发现:客户端负责决定可用服务实例的网络地址,并且在集群中对请求负载均衡, 客户端访问服务登记表,也就是一个可用服务的数据库,然后客户端使用一种负载均衡算法选择一个可用的服务实例然后发起请求。

客户端发现相对于服务端发现最大的区别是:客户端知道(缓存)可用服务注册表信息。如果Client端缓存没能从服务端及时更新的话,可能出现Client 与 服务端缓存数据不一致的情况。
#二、网关与Eureka结合使用

Netflix OSS 提供了一个客户端服务发现的好例子。Eureka Server 为注册中心,Zuul 相对于Eureka Server来说是Eureka Client,Zuul 会把 Eureka Server 端服务列表缓存到本地,并以定时任务的形式更新服务列表,同时 Zuul 通过本地列表发现其它服务,使用 Ribbon 实现客户端负载均衡。如果你想和更多Spring Cloud技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态
1.jpg

正常情况下,调用方对网关发起请求即刻能得到响应。但是当对生产者做缩容、下线、升级的情况下,由于Eureka这种多级缓存的设计结构和定时更新的机制,LoadBalance 端的服务列表B存在更新不及时的情况(由上篇文章《Eureka 缓存机制》可知,服务消费者最长感知时间将无限趋近240s),如果这时消费者对网关发起请求,LoadBalance 会对一个已经不存在的服务发起请求,请求是会超时的。
#三、解决方案

##实现思路

生产者下线后,最先得到感知的是 Eureka Server 中的 readWriteCacheMap,最后得到感知的是网关核心中的 LoadBalance。但是 loadBalance 对生产者的发现是在 loadBalance 本地维护的列表中。

所以要想达到网关对生产者下线的实时感知,可以这样做:首先生产者或者部署平台主动通知 Eureka Server,然后跳过 Eureka 多级缓存之间的更新时间,直接通知 Zuul 中的 Eureka Client,最后将 Eureka Client 中的服务列表更新到 Ribbon 中。

但是如果下线通知的逻辑代码放在生产者中,会造成代码污染、语言差异等问题。

借用一句名言:计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决。
2.jpg

Gateway-SynchSpeed 相当于一个代理服务,它对外提供REST API来负责响应调用方的下线请求,同时会将生产者的状态同步到 Eureka Server 和 网关核心,起着 状态同步 和 软事物 的作用。

思路:在生产者做 缩容、下线、升级 前,spider 平台(spider为容器管理平台)会主动通知 Gateway-SynchSpeed 某个生产者的某个实例要下线了,然后 Gateway-SynchSpeed 会通知 Eureka Server 生产者的某个实例下线了;如果Eureka Server 下线成功,Gateway-SynchSpeed 会直接通知 网关核心。

设计特点:

* 无侵入性、方便使用。不用关心调用方的基于何种语言实现,调用者只要对 Gateway-SynchSpeed 发起一个http rest请求即可,真正的实现逻辑不用侵入到调用方而是交给这个代理来实现。
* 原子性。调用方先在Eureka Server下线,然后在所有相关网关核心中下线为最小工作执行单元,Gateway-SynchSpeed 相当于一个"软事物",保证服务下线的某种程度上原子特性。

##实现步骤

3.jpg

步骤说明:

第一步:在生产者做 缩容、下线、升级 前,spider平台会以http请求的形式通知到 Gateway-SynchSpeed 服务,通知的粒度为服务实例所在的容器IP。

第二步:Gateway-SynchSpeed 接受到请求后,先校验IP的可用性,然后通知Eureka Server。

第三步:Eureka Server 将 Producer 置为失效状态,并返回处理结果(Eureka 下线形式分为两种,一种是直接从服务注册列表直接剔除,第二种是状态下线,即是将 Producer 的状态置为OUT_OF_SERVICE。 如果是以第一种形式下线,Spider平台发出下线请求后,不能保证Producer进程立刻被kill,如果这期间 Producer 还有心跳同步到 Eureka Server,服务会重新注册到 Eureka Server)。

第四步:Gateway-SynchSpeed 得到上一步结果,如果结果为成功,则执行下一步;反之,则停止。

第五步:Gateway-SynchSpeed 为Eureka Client。Gateway-SynchSpeed 通过 IP 到本地服务注册列表中得到 Producer 的 Application-Name。

第六步:Gateway-SynchSpeed 通过 Application-Name 到网关核心库中查询所有与下线服务相关的 网关组名字。

第七步:Gateway-SynchSpeed 通过 网关组名字 到本地服务列表中查找网关组下所有的服务地址 ipAddress(ip : port)。

第八步:Gateway-SynchSpeed 异步通知所有相关网关节点。

第九步:Gateway-Core 收到通知后,对 Producer 做状态下线,同时记录所有状态下线成功的实例信息到缓存 DownServiceCache 中。

第十步:Gateway-Core 更新本地 Ribbon 服务列表。
#四、补偿机制

Eureka 提供了一种安全保护机制。Eureka Client 从 Eureka Server 更新服务列表前,会校验相关Hash值是否改变(Client 服务列表被修改,hash值会改变),如果改变,更新方式会从增量更新变成全量更新,(由《Eureka 缓存机制》可知这30s内 readOnlyCacheMap 和 readWriteCacheMap 的数据可能存在差异),如果Client端缓存列表被readOnlyCacheMap 覆盖,最终会导致 Ribbon 端服务列表与 readWriteCacheMap 数据不一致。
4.jpg

针对 Eureka 这种机制,引入监听器 EurekaEventListener 作为补偿机制,它会监听 Eureka Client 全量拉取事件,对于缓存中未超过30s的服务,将其状态重新设置成 OUT_OF_SERVICE 。
#五、API安全设计

考虑到系统的安全性问题,如果被人恶意访问,可能会使生产者在Eureka Server中无故下线,导致消费者无法通过 Eureka Server 来发现生产者。

使用黑白名单做安全过滤,基本流程如下:

* 对 Gateway-Synchspeed 中设置白名单网段(IP网段)。
* 在 Gateway-Synchspeed 加入过滤器,对下线请求方进行IP校验,如果请求端IP在网段中,则放行;反之,过滤。

#六、日志回溯

由于 Gateway-SynchSpeed 和 Gateway-Core 是部署在 Docker 容器中,如果容器重启,会导致日志文件全部丢失。所以需要将 Gateway-SynchSpeed 和 Gateway-Core 中相关日志写入到 Elasticsearch ,最终由 Kibana 负责查询 Elasticsearch 的数据并以可视化的方式展现。
#七、代码片段展示

Gateway-SynchSpeed 做状态同步。
5.jpg

EurekaEventListener 处理缓存数据。
6.jpg

#八、 补充说明

目前网关实现对服务下线的实时感知中,使用的 Zuul 和 Eureka 版本为 Spring Cloud Zuul 1.3.6.RELEASE、Spring Cloud Eureka 1.4.4.RELEASE。

目前网关实现的是对网关下游服务的实时感知,而且需满足以下条件:

* 生产者需部署在 kubernetes 容器管理平台 。
* 生产者做正常的下线、升级或者缩容操作。如果是由于容器资源不足,导致服务异常宕机等非正常下线,不支持。

网关服务下线实时感知是网关对业务方提供的一种可选的解决方案,在 spider 平台中默认是没有开启此功能,是否开启此功能由业务方根据本身系统要求决定,具体如何配置可参考 API网关接入指南 中 《网关实时感知在spider上配置文档说明》。

原文链接:http://college.creditease.cn/detail/256

详解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

把数据库放入Docker是一个好主意吗?

Andy_Lee 发表了文章 • 0 个评论 • 373 次浏览 • 2019-06-04 11:33 • 来自相关话题

服务端高并发分布式架构演进之路

翔宇 发表了文章 • 0 个评论 • 248 次浏览 • 2019-06-04 11:16 • 来自相关话题

【编者的话】本文以淘宝作为例子,介绍从一百个并发到千万级并发情况下服务端的架构的演进过程,同时列举出每个演进阶段会遇到的相关技术,让大家对架构的演进有一个整体的认知,文章最后汇总了一些架构设计的原则。 #基本概念 在介绍架构之前,为 ...查看全部
【编者的话】本文以淘宝作为例子,介绍从一百个并发到千万级并发情况下服务端的架构的演进过程,同时列举出每个演进阶段会遇到的相关技术,让大家对架构的演进有一个整体的认知,文章最后汇总了一些架构设计的原则。
#基本概念

在介绍架构之前,为了避免部分读者对架构设计中的一些概念不了解,下面对几个最基础的概念进行介绍:
##分布式

系统中的多个模块在不同服务器上部署,即可称为分布式系统,如Tomcat和数据库分别部署在不同的服务器上,或两个相同功能的Tomcat分别部署在不同服务器上。
##高可用

系统中部分节点失效时,其他节点能够接替它继续提供服务,则可认为系统具有高可用性。
##集群

一个特定领域的软件部署在多台服务器上并作为一个整体提供一类服务,这个整体称为集群。如Zookeeper中的Master和Slave分别部署在多台服务器上,共同组成一个整体提供集中配置服务。在常见的集群中,客户端往往能够连接任意一个节点获得服务,并且当集群中一个节点掉线时,其他节点往往能够自动的接替它继续提供服务,这时候说明集群具有高可用性。
##负载均衡

请求发送到系统时,通过某些方式把请求均匀分发到多个节点上,使系统中每个节点能够均匀的处理请求负载,则可认为系统是负载均衡的。
##正向代理和反向代理

系统内部要访问外部网络时,统一通过一个代理服务器把请求转发出去,在外部网络看来就是代理服务器发起的访问,此时代理服务器实现的是正向代理;当外部请求进入系统时,代理服务器把该请求转发到系统中的某台服务器上,对外部请求来说,与之交互的只有代理服务器,此时代理服务器实现的是反向代理。简单来说,正向代理是代理服务器代替系统内部来访问外部网络的过程,反向代理是外部请求访问系统时通过代理服务器转发到内部服务器的过程。
#架构演进

##单机架构

1.png

以淘宝作为例子。在网站最初时,应用数量与用户数都较少,可以把Tomcat和数据库部署在同一台服务器上。浏览器往www.taobao.com发起请求时,首先经过DNS服务器(域名系统)把域名转换为实际IP地址10.102.4.1,浏览器转而访问该IP对应的Tomcat。如果你想和更多Tomcat技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态

随着用户数的增长,Tomcat和数据库之间竞争资源,单机性能不足以支撑业务
##第一次演进:Tomcat与数据库分开部署

2.png

Tomcat和数据库分别独占服务器资源,显著提高两者各自性能。

随着用户数的增长,并发读写数据库成为瓶颈
##第二次演进:引入本地缓存和分布式缓存

3.png

在Tomcat同服务器上或同JVM中增加本地缓存,并在外部增加分布式缓存,缓存热门商品信息或热门商品的html页面等。通过缓存能把绝大多数请求在读写数据库前拦截掉,大大降低数据库压力。其中涉及的技术包括:使用memcached作为本地缓存,使用Redis作为分布式缓存,还会涉及缓存一致性、缓存穿透/击穿、缓存雪崩、热点数据集中失效等问题。

缓存抗住了大部分的访问请求,随着用户数的增长,并发压力主要落在单机的Tomcat上,响应逐渐变慢
##第三次演进:引入反向代理实现负载均衡

4.png

在多台服务器上分别部署Tomcat,使用反向代理软件(Nginx)把请求均匀分发到每个Tomcat中。此处假设Tomcat最多支持100个并发,Nginx最多支持50000个并发,那么理论上Nginx把请求分发到500个Tomcat上,就能抗住50000个并发。其中涉及的技术包括:Nginx、HAProxy,两者都是工作在网络第七层的反向代理软件,主要支持http协议,还会涉及session共享、文件上传下载的问题。

反向代理使应用服务器可支持的并发量大大增加,但并发量的增长也意味着更多请求穿透到数据库,单机的数据库最终成为瓶颈
##第四次演进:数据库读写分离

5.png

把数据库划分为读库和写库,读库可以有多个,通过同步机制把写库的数据同步到读库,对于需要查询最新写入数据场景,可通过在缓存中多写一份,通过缓存获得最新数据。其中涉及的技术包括:Mycat,它是数据库中间件,可通过它来组织数据库的分离读写和分库分表,客户端通过它来访问下层数据库,还会涉及数据同步,数据一致性的问题。

业务逐渐变多,不同业务之间的访问量差距较大,不同业务直接竞争数据库,相互影响性能
##第五次演进:数据库按业务分库

6.png

把不同业务的数据保存到不同的数据库中,使业务之间的资源竞争降低,对于访问量大的业务,可以部署更多的服务器来支撑。这样同时导致跨业务的表无法直接做关联分析,需要通过其他途径来解决,但这不是本文讨论的重点,有兴趣的可以自行搜索解决方案。

随着用户数的增长,单机的写库会逐渐会达到性能瓶颈
##第六次演进:把大表拆分为小表

7.png

比如针对评论数据,可按照商品ID进行hash,路由到对应的表中存储;针对支付记录,可按照小时创建表,每个小时表继续拆分为小表,使用用户ID或记录编号来路由数据。只要实时操作的表数据量足够小,请求能够足够均匀的分发到多台服务器上的小表,那数据库就能通过水平扩展的方式来提高性能。其中前面提到的Mycat也支持在大表拆分为小表情况下的访问控制。

这种做法显著的增加了数据库运维的难度,对DBA的要求较高。数据库设计到这种结构时,已经可以称为分布式数据库,但是这只是一个逻辑的数据库整体,数据库里不同的组成部分是由不同的组件单独来实现的,如分库分表的管理和请求分发,由Mycat实现,SQL的解析由单机的数据库实现,读写分离可能由网关和消息队列来实现,查询结果的汇总可能由数据库接口层来实现等等,这种架构其实是MPP(大规模并行处理)架构的一类实现。

目前开源和商用都已经有不少MPP数据库,开源中比较流行的有Greenplum、TiDB、Postgresql XC、HAWQ等,商用的如南大通用的GBase、睿帆科技的雪球DB、华为的LibrA等等,不同的MPP数据库的侧重点也不一样,如TiDB更侧重于分布式OLTP场景,Greenplum更侧重于分布式OLAP场景,这些MPP数据库基本都提供了类似Postgresql、Oracle、MySQL那样的SQL标准支持能力,能把一个查询解析为分布式的执行计划分发到每台机器上并行执行,最终由数据库本身汇总数据进行返回,也提供了诸如权限管理、分库分表、事务、数据副本等能力,并且大多能够支持100个节点以上的集群,大大降低了数据库运维的成本,并且使数据库也能够实现水平扩展。

数据库和Tomcat都能够水平扩展,可支撑的并发大幅提高,随着用户数的增长,最终单机的Nginx会成为瓶颈
##第七次演进:使用LVS或F5来使多个Nginx负载均衡

8.png

由于瓶颈在Nginx,因此无法通过两层的Nginx来实现多个Nginx的负载均衡。图中的LVS和F5是工作在网络第四层的负载均衡解决方案,其中LVS是软件,运行在操作系统内核态,可对TCP请求或更高层级的网络协议进行转发,因此支持的协议更丰富,并且性能也远高于Nginx,可假设单机的LVS可支持几十万个并发的请求转发;F5是一种负载均衡硬件,与LVS提供的能力类似,性能比LVS更高,但价格昂贵。由于LVS是单机版的软件,若LVS所在服务器宕机则会导致整个后端系统都无法访问,因此需要有备用节点。可使用keepalived软件模拟出虚拟IP,然后把虚拟IP绑定到多台LVS服务器上,浏览器访问虚拟IP时,会被路由器重定向到真实的LVS服务器,当主LVS服务器宕机时,keepalived软件会自动更新路由器中的路由表,把虚拟IP重定向到另外一台正常的LVS服务器,从而达到LVS服务器高可用的效果。

此处需要注意的是,上图中从Nginx层到Tomcat层这样画并不代表全部Nginx都转发请求到全部的Tomcat,在实际使用时,可能会是几个Nginx下面接一部分的Tomcat,这些Nginx之间通过keepalived实现高可用,其他的Nginx接另外的Tomcat,这样可接入的Tomcat数量就能成倍的增加。

由于LVS也是单机的,随着并发数增长到几十万时,LVS服务器最终会达到瓶颈,此时用户数达到千万甚至上亿级别,用户分布在不同的地区,与服务器机房距离不同,导致了访问的延迟会明显不同
##第八次演进:通过DNS轮询实现机房间的负载均衡

9.png

在DNS服务器中可配置一个域名对应多个IP地址,每个IP地址对应到不同的机房里的虚拟IP。当用户访问www.taobao.com时,DNS服务器会使用轮询策略或其他策略,来选择某个IP供用户访问。此方式能实现机房间的负载均衡,至此,系统可做到机房级别的水平扩展,千万级到亿级的并发量都可通过增加机房来解决,系统入口处的请求并发量不再是问题。

随着数据的丰富程度和业务的发展,检索、分析等需求越来越丰富,单单依靠数据库无法解决如此丰富的需求
##第九次演进:引入NoSQL数据库和搜索引擎等技术

10.png

当数据库中的数据多到一定规模时,数据库就不适用于复杂的查询了,往往只能满足普通查询的场景。对于统计报表场景,在数据量大时不一定能跑出结果,而且在跑复杂查询时会导致其他查询变慢,对于全文检索、可变数据结构等场景,数据库天生不适用。因此需要针对特定的场景,引入合适的解决方案。如对于海量文件存储,可通过分布式文件系统HDFS解决,对于key value类型的数据,可通过HBase和Redis等方案解决,对于全文检索场景,可通过搜索引擎如ElasticSearch解决,对于多维分析场景,可通过Kylin或Druid等方案解决。

当然,引入更多组件同时会提高系统的复杂度,不同的组件保存的数据需要同步,需要考虑一致性的问题,需要有更多的运维手段来管理这些组件等。

引入更多组件解决了丰富的需求,业务维度能够极大扩充,随之而来的是一个应用中包含了太多的业务代码,业务的升级迭代变得困难
##第十次演进:大应用拆分为小应用

11.png

按照业务板块来划分应用代码,使单个应用的职责更清晰,相互之间可以做到独立升级迭代。这时候应用之间可能会涉及到一些公共配置,可以通过分布式配置中心Zookeeper来解决。

不同应用之间存在共用的模块,由应用单独管理会导致相同代码存在多份,导致公共功能升级时全部应用代码都要跟着升级
##第十一次演进:复用的功能抽离成微服务

12.png

如用户管理、订单、支付、鉴权等功能在多个应用中都存在,那么可以把这些功能的代码单独抽取出来形成一个单独的服务来管理,这样的服务就是所谓的微服务,应用和服务之间通过HTTP、TCP或RPC请求等多种方式来访问公共服务,每个单独的服务都可以由单独的团队来管理。此外,可以通过Dubbo、SpringCloud等框架实现服务治理、限流、熔断、降级等功能,提高服务的稳定性和可用性。

不同服务的接口访问方式不同,应用代码需要适配多种访问方式才能使用服务,此外,应用访问服务,服务之间也可能相互访问,调用链将会变得非常复杂,逻辑变得混乱
##第十二次演进:引入企业服务总线ESB屏蔽服务接口的访问差异

13.png

通过ESB统一进行访问协议转换,应用统一通过ESB来访问后端服务,服务与服务之间也通过ESB来相互调用,以此降低系统的耦合程度。这种单个应用拆分为多个应用,公共服务单独抽取出来来管理,并使用企业消息总线来解除服务之间耦合问题的架构,就是所谓的SOA(面向服务)架构,这种架构与微服务架构容易混淆,因为表现形式十分相似。个人理解,微服务架构更多是指把系统里的公共服务抽取出来单独运维管理的思想,而SOA架构则是指一种拆分服务并使服务接口访问变得统一的架构思想,SOA架构中包含了微服务的思想。

业务不断发展,应用和服务都会不断变多,应用和服务的部署变得复杂,同一台服务器上部署多个服务还要解决运行环境冲突的问题,此外,对于如大促这类需要动态扩缩容的场景,需要水平扩展服务的性能,就需要在新增的服务上准备运行环境,部署服务等,运维将变得十分困难。
##第十三次演进:引入容器化技术实现运行环境隔离与动态服务管理

14.png

目前最流行的容器化技术是Docker,最流行的容器管理服务是Kubernetes(K8S),应用/服务可以打包为Docker镜像,通过K8S来动态分发和部署镜像。Docker镜像可理解为一个能运行你的应用/服务的最小的操作系统,里面放着应用/服务的运行代码,运行环境根据实际的需要设置好。把整个“操作系统”打包为一个镜像后,就可以分发到需要部署相关服务的机器上,直接启动Docker镜像就可以把服务起起来,使服务的部署和运维变得简单。

在大促的之前,可以在现有的机器集群上划分出服务器来启动Docker镜像,增强服务的性能,大促过后就可以关闭镜像,对机器上的其他服务不造成影响(在3.14节之前,服务运行在新增机器上需要修改系统配置来适配服务,这会导致机器上其他服务需要的运行环境被破坏)。

使用容器化技术后服务动态扩缩容问题得以解决,但是机器还是需要公司自身来管理,在非大促的时候,还是需要闲置着大量的机器资源来应对大促,机器自身成本和运维成本都极高,资源利用率低
##第十四次演进:以云平台承载系统

15.png

系统可部署到公有云上,利用公有云的海量机器资源,解决动态硬件资源的问题,在大促的时间段里,在云平台中临时申请更多的资源,结合Docker和K8S来快速部署服务,在大促结束后释放资源,真正做到按需付费,资源利用率大大提高,同时大大降低了运维成本。

所谓的云平台,就是把海量机器资源,通过统一的资源管理,抽象为一个资源整体,在之上可按需动态申请硬件资源(如CPU、内存、网络等),并且之上提供通用的操作系统,提供常用的技术组件(如Hadoop技术栈,MPP数据库等)供用户使用,甚至提供开发好的应用,用户不需要关系应用内部使用了什么技术,就能够解决需求(如音视频转码服务、邮件服务、个人博客等)。在云平台中会涉及如下几个概念:

* IaaS:基础设施即服务。对应于上面所说的机器资源统一为资源整体,可动态申请硬件资源的层面;
* PaaS:平台即服务。对应于上面所说的提供常用的技术组件方便系统的开发和维护;
* SaaS:软件即服务。对应于上面所说的提供开发好的应用或服务,按功能或性能要求付费。

至此,以上所提到的从高并发访问问题,到服务的架构和系统实施的层面都有了各自的解决方案,但同时也应该意识到,在上面的介绍中,其实是有意忽略了诸如跨机房数据同步、分布式事务实现等等的实际问题,这些问题以后有机会再拿出来单独讨论。
#架构设计总结

##架构的调整是否必须按照上述演变路径进行?

不是的,以上所说的架构演变顺序只是针对某个侧面进行单独的改进,在实际场景中,可能同一时间会有几个问题需要解决,或者可能先达到瓶颈的是另外的方面,这时候就应该按照实际问题实际解决。如在政府类的并发量可能不大,但业务可能很丰富的场景,高并发就不是重点解决的问题,此时优先需要的可能会是丰富需求的解决方案。
##对于将要实施的系统,架构应该设计到什么程度?

对于单次实施并且性能指标明确的系统,架构设计到能够支持系统的性能指标要求就足够了,但要留有扩展架构的接口以便不备之需。对于不断发展的系统,如电商平台,应设计到能满足下一阶段用户量和性能指标要求的程度,并根据业务的增长不断的迭代升级架构,以支持更高的并发和更丰富的业务。
##服务端架构和大数据架构有什么区别?

所谓的“大数据”其实是海量数据采集清洗转换、数据存储、数据分析、数据服务等场景解决方案的一个统称,在每一个场景都包含了多种可选的技术,如数据采集有Flume、Sqoop、Kettle等,数据存储有分布式文件系统HDFS、FastDFS,NoSQL数据库HBase、MongoDB等,数据分析有Spark技术栈、机器学习算法等。总的来说大数据架构就是根据业务的需求,整合各种大数据组件组合而成的架构,一般会提供分布式存储、分布式计算、多维分析、数据仓库、机器学习算法等能力。而服务端架构更多指的是应用组织层面的架构,底层能力往往是由大数据架构来提供。
#有没有一些架构设计的原则?


* N+1设计。系统中的每个组件都应做到没有单点故障;
* 回滚设计。确保系统可以向前兼容,在系统升级时应能有办法回滚版本;
* 禁用设计。应该提供控制具体功能是否可用的配置,在系统出现故障时能够快速下线功能;
* 监控设计。在设计阶段就要考虑监控的手段;
* 多活数据中心设计。若系统需要极高的高可用,应考虑在多地实施数据中心进行多活,至少在一个机房断电的情况下系统依然可用;
* 采用成熟的技术。刚开发的或开源的技术往往存在很多隐藏的bug,出了问题没有商业支持可能会是一个灾难;
* 资源隔离设计。应避免单一业务占用全部资源;
* 架构应能水平扩展。系统只有做到能水平扩展,才能有效避免瓶颈问题;
* 非核心则购买。非核心功能若需要占用大量的研发资源才能解决,则考虑购买成熟的产品;
* 使用商用硬件。商用硬件能有效降低硬件故障的机率;
* 快速迭代。系统应该快速开发小功能模块,尽快上线进行验证,早日发现问题大大降低系统交付的风险;
* 无状态设计。服务接口应该做成无状态的,当前接口的访问不依赖于接口上次访问的状态。

原文链接:https://segmentfault.com/a/1190000018626163

介绍一个小工具:Kubedog

尼古拉斯 发表了文章 • 0 个评论 • 178 次浏览 • 2019-06-04 10:46 • 来自相关话题

Kubedog 是一个开源的 Golang 项目,使用 watch 方式对 Kubernetes 资源进行跟踪,能够方便的用于日常运维和 CI/CD 过程之中,项目中除了一个 CLI 小工具之外,还提供了一组 SDK,用户可以将其中的 Watch 功能集成到自 ...查看全部
Kubedog 是一个开源的 Golang 项目,使用 watch 方式对 Kubernetes 资源进行跟踪,能够方便的用于日常运维和 CI/CD 过程之中,项目中除了一个 CLI 小工具之外,还提供了一组 SDK,用户可以将其中的 Watch 功能集成到自己的系统之中。安装过程非常简单,在项目网页直接下载即可。如果你想和更多Kubernetes技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态

Kubedog CLI 有两个功能:rollout track 和 follow。
#rollout track
在 Kubernetes 上运行应用时,通常的做法是使用 kubectl apply 提交 YAML 之后,使用 kubectl get -w 或者 watch kubectl get 之类的命令等待 Pod 启动。如果启动成功,则进行测试等后续动作;如果启动失败,就需要用 kubectl logs、kubectl describe 等命令来查看失败原因。kubedog 能在一定程度上简化这一过程。

例如使用 kubectl run 命令创建一个新的 Deployment 资源,并使用 kubedog 跟进创建进程:
$ kubectl run nginx --image=nginx22
...
deployment.apps/nginx created

$ kubedog rollout track deployment nginx
# deploy/nginx added
# deploy/nginx rs/nginx-6cc78cbf64 added
# deploy/nginx po/nginx-6cc78cbf64-8pnjz added
# deploy/nginx po/nginx-6cc78cbf64-8pnjz nginx error: ImagePullBackOff: Back-off pulling image "nginx22"
deploy/nginx po/nginx-6cc78cbf64-8pnjz nginx failed: ImagePullBackOff: Back-off pulling image "nginx22"

$ echo $?
130

很方便的看出,运行失败的状态及其原因,并且可以使用返回码来进行判断,方便在 Pipeline 中的运行。接下来可以使用 kubectl edit 命令编辑 Deployment,修改正确的镜像名称。然后再次进行验证:
$ kubectl edit deployment nginx
deployment.extensions/nginx edited
$ kubedog rollout track deployment nginx
# deploy/nginx added
# deploy/nginx rs/nginx-dbddb74b8 added
# deploy/nginx po/nginx-dbddb74b8-x4nkm added
# deploy/nginx event: po/nginx-dbddb74b8-x4nkm Pulled: Successfully pulled image "nginx"
# deploy/nginx event: po/nginx-dbddb74b8-x4nkm Created: Created container
# deploy/nginx event: po/nginx-dbddb74b8-x4nkm Started: Started container
# deploy/nginx event: ScalingReplicaSet: Scaled down replica set nginx-6cc78cbf64 to 0
# deploy/nginx become READY
$ echo $?
0

修改完成,重新运行 kubedog,会看到成功运行的情况,并且返回值也变成了 0。
#follow
follow 命令的功能和 kubetail 的功能有少量重叠,可以用 Deployment/Job/Daemonset 等为单位,查看其中所有 Pod 的日志,例如前面用的 Nginx,如果有访问的话,就会看到如下结果:
$ kubedog follow deployment nginx
# deploy/nginx appears to be ready
# deploy/nginx rs/nginx-6cc78cbf64 added
# deploy/nginx new rs/nginx-dbddb74b8 added
# deploy/nginx rs/nginx-dbddb74b8(new) po/nginx-dbddb74b8-x4nkm added
# deploy/nginx rs/nginx-6cc54845d9 added
# deploy/nginx event: ScalingReplicaSet: Scaled up replica set nginx-6cc54845d9 to 1
# deploy/nginx rs/nginx-6cc54845d9(new) po/nginx-6cc54845d9-nhlvs added
# deploy/nginx event: po/nginx-6cc54845d9-nhlvs Pulling: pulling image "nginx:alpine"
# deploy/nginx event: po/nginx-6cc54845d9-nhlvs Pulled: Successfully pulled image "nginx:alpine"
# deploy/nginx event: po/nginx-6cc54845d9-nhlvs Created: Created container
# deploy/nginx event: po/nginx-6cc54845d9-nhlvs Started: Started container
# deploy/nginx event: ScalingReplicaSet: Scaled down replica set nginx-dbddb74b8 to 0
# deploy/nginx become READY
# deploy/nginx event: po/nginx-dbddb74b8-x4nkm Killing: Killing container with id docker://nginx:Need to kill Pod
[quote]> deploy/nginx rs/nginx-dbddb74b8 po/nginx-dbddb74b8-x4nkm nginx[/quote]

[quote]> deploy/nginx rs/nginx-6cc54845d9(new) po/nginx-6cc54845d9-nhlvs nginx
127.0.0.1 - - [02/Jun/2019:11:35:08 +0000] "GET / HTTP/1.1" 200 612 "-" "Wget" "-"
127.0.0.1 - - [02/Jun/2019:11:35:11 +0000] "GET / HTTP/1.1" 200 612 "-" "Wget" "-"

项目地址
https://github.com/flant/kubedog[/quote]

原文链接:https://blog.fleeto.us/post/intro-kubedog/

ZooKeeper开发分布式系统,动态服务上下线感知

JetLee 发表了文章 • 0 个评论 • 184 次浏览 • 2019-06-04 10:31 • 来自相关话题

#什么是ZooKeeper ZooKeeper是一个分布式开源框架,提供了协调分布式应用的基本服务,它向外部应用暴露一组通用服务——分布式同步(Distributed Synchronization)、命名服务(Naming Service)、集 ...查看全部
#什么是ZooKeeper

ZooKeeper是一个分布式开源框架,提供了协调分布式应用的基本服务,它向外部应用暴露一组通用服务——分布式同步(Distributed Synchronization)、命名服务(Naming Service)、集群维护(Group Maintenance)等,简化分布式应用协调及其管理的难度,提供高性能的分布式服务。ZooKeeper本身可以以单机模式安装运行,不过它的长处在于通过分布式ZooKeeper集群(一个Leader,多个Follower),基于一定的策略来保证ZooKeeper集群的稳定性和可用性,从而实现分布式应用的可靠性。
#Zookeeper简介


  1. ZooKeeper是为别的分布式程序服务的
  2. ZooKeeper本身就是一个分布式程序(只要有半数以上节点存活,ZooKeeper就能正常服务)
  3. ZooKeeper所提供的服务涵盖:主从协调、服务器节点动态上下线、统一配置管理、分布式共享锁、统> 一名称服务等
  4. 虽然说可以提供各种服务,但是ZooKeeper在底层其实只提供了两个功能:

1. 管理(存储,读取)用户程序提交的数据(类似namenode中存放的metadata)
2. 为用户程序提供数据节点监听服务

#ZooKeeper应用场景图

1.jpg

2.jpg

#ZooKeeper集群机制

ZooKeeper集群的角色: Leader 和 follower

只要集群中有半数以上节点存活,集群就能提供服务。
#ZooKeeper特性


  1. ZooKeeper:一个leader,多个follower组成的集群
  2. 全局数据一致:每个server保存一份相同的数据副本,client无论连接到哪个server,数据都是一致的
  3. 分布式读写,更新请求转发,由leader实施
  4. 更新请求顺序进行,来自同一个client的更新请求按其发送顺序依次执行
  5. 数据更新原子性,一次数据更新要么成功,要么失败
  6. 实时性,在一定时间范围内,client能读到最新数据

#ZooKeeper的数据存储机制

##数据存储形式

ZooKeeper中对用户的数据采用kv形式存储。如果你想和更多ZooKeeper技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态

只是ZooKeeper有点特别:

key:是以路径的形式表示的,那就以为着,各key之间有父子关系,比如:

* / 是顶层key
* 用户建的key只能在/ 下作为子节点,比如建一个key: /aa 这个key可以带value数据
* 也可以建一个key: /bb
* 也可以建key: /aa/xx

ZooKeeper中,对每一个数据key,称作一个znode

综上所述,ZooKeeper中的数据存储形式如下:
3.jpg

##znode类型

ZooKeeper中的znode有多种类型:

  1. PERSISTENT 持久的:创建者就算跟集群断开联系,该类节点也会持久存在与zk集群中
  2. EPHEMERAL 短暂的:创建者一旦跟集群断开联系,zk就会将这个节点删除
  3. SEQUENTIAL 带序号的:这类节点,zk会自动拼接上一个序号,而且序号是递增的

组合类型:

* PERSISTENT :持久不带序号
* EPHEMERAL :短暂不带序号
* PERSISTENT 且 SEQUENTIAL :持久且带序号
* EPHEMERAL 且 SEQUENTIAL :短暂且带序号

#ZooKeeper的集群部署

集群选举示意图:
4.jpg

解压ZooKeeper安装包到apps目录下:
tar -zxvf zookeeper-3.4.6.tar.gz -C apps

cd /root/apps/zookeeper-3.4.6/conf
cp zoo_sample.cfg zoo.cfg
vi zoo.cfg

修改dataDir=/root/zkdata

在后面加上集群的机器:2888是leader和follower通讯端口,3888是投票的
server.1=hdp-01:2888:3888
server.2=hdp-02:2888:3888
server.3=hdp-03:2888:3888

对3台节点,都创建目录 mkdir /root/zkdata

对3台节点,在工作目录中生成myid文件,但内容要分别为各自的id: 1,2,3
echo 1 > /root/zkdata/myid
echo 2 > /root/zkdata/myid
echo 3 > /root/zkdata/myid

从hdp20-01上scp安装目录到其他两个节点
cd apps
scp -r zookeeper-3.4.6/ hdp-02:$PWD
scp -r zookeeper-3.4.6/ hdp-03:$PWD

启动ZooKeeper集群

ZooKeeper没有提供自动批量启动脚本,需要手动一台一台地起ZooKeeper进程

在每一台节点上,运行命令:
cd /root/apps/zookeeper-3.4.6
bin/zkServer.sh start

启动后,用jps应该能看到一个进程:QuorumPeerMain

但是,光有进程不代表zk已经正常服务,需要用命令检查状态:
bin/zkServer.sh status

能看到角色模式:为leader或follower,即正常了。

自己写个脚本,一键启动
vi zkmanage.sh

#!/bin/bash
for host in hdp-01 hdp-02 hdp-03
do
echo "${host}:$1ing....."
ssh $host "/root/apps/zookeeper-3.4.6/bin/zkServer.sh $1"
done

停止命令:sh zjmanage.sh stop

加个可执行权限:chmod +zkmanage.sh

启动命令:./zkmanage.sh start

但是出现没有Java环境变量问题,修改配置文件
vi zkmanage.sh

修改配置如下:
#!/bin/bash
for host in hdp-01 hdp-02 hdp-03
do
echo "${host}:$1ing....."
ssh $host "source /etc/profile;/root/apps/zookeeper-3.4.6/bin/zkServer.sh $1"
done

sleep 2
for host in hdp-01 hdp-02 hdp-03
do
ssh $host "source /etc/profile;/root/apps/zookeeper-3.4.6/bin/zkServer.sh status"
done

启动集群结果:
hdp-01:starting.....
JMX enabled by default
Using config: /root/apps/zookeeper-3.4.6/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED
hdp-02:starting.....
JMX enabled by default
Using config: /root/apps/zookeeper-3.4.6/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED
hdp-03:starting.....
JMX enabled by default
Using config: /root/apps/zookeeper-3.4.6/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED
JMX enabled by default
Using config: /root/apps/zookeeper-3.4.6/bin/../conf/zoo.cfg
Mode: follower
JMX enabled by default
Using config: /root/apps/zookeeper-3.4.6/bin/../conf/zoo.cfg
Mode: leader
JMX enabled by default
Using config: /root/apps/zookeeper-3.4.6/bin/../conf/zoo.cfg
Mode: follower

ZooKeeper的Java客户端操作代码:
public class ZookeeperCliDemo {
ZooKeeper zk =null;
@Before
public void init() throws Exception {
zk=new ZooKeeper("hdp-01:2181,hdp-02:2181,hdp-03:2181", 2000, null);
}
@Test
public void testCreate() throws Exception {
//参数1:要创建的节点路径;参数2:数据;参数3:访问权限;参数4:节点类型
String create = zk.create("/eclipse", "hello eclipse".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
System.out.println(create);
zk.close();
}

@Test
public void testUpdate() throws Exception {
//参数1:节点路径;参数2:数据;参数3:所要修改的版本,-1表示任意版本
zk.setData("/eclipse","我喜欢青青".getBytes(),-1);
zk.close();
}
@Test
public void testGet() throws Exception {
//参数1:节点路径;参数2:事件监听;参数3:所要修改的版本,null表示最新版本
byte[] data = zk.getData("/eclipse", false, null);
System.out.println(new String(data,"UTF-8"));
zk.close();
}

@Test
public void testListChildren() throws KeeperException, InterruptedException {
//参数1:节点路径;参数2:是否要监听
//注意:返回的结果只有子节点的名字,不带全路径
List children = zk.getChildren("/cc", false);
for(String child:children){
System.out.println(child);
}
zk.close();
}

@Test
public void testRm() throws KeeperException, InterruptedException {
zk.delete("/eclipse",-1);
zk.close();
}
}

ZooKeeper监听功能代码:
public class ZookeeperWatchDemo {
ZooKeeper zk =null;
@Before
public void init() throws Exception {
zk=new ZooKeeper("hdp-01:2181,hdp-02:2181,hdp-03:2181", 2000, new Watcher() {
public void process(WatchedEvent watchedEvent) {
if (watchedEvent.getState() == Event.KeeperState.SyncConnected
&& watchedEvent.getType() == Event.EventType.NodeDataChanged) {
System.out.println("收到事件所发生节点的路径" + watchedEvent.getPath());
System.out.println("收到事件所发生节点的状态" + watchedEvent.getState());
System.out.println("收到事件所发生节点的类型" + watchedEvent.getType());
System.out.println("watch事件通知。。换照片");
try {
zk.getData("/mygirls", true, null);
} catch (Exception e) {
e.printStackTrace();
}
}else if(watchedEvent.getState()==Event.KeeperState.SyncConnected &&
watchedEvent.getType()==Event.EventType.NodeChildrenChanged){
System.out.println("收到事件所发生节点的路径" + watchedEvent.getPath());
System.out.println("收到事件所发生节点的状态" + watchedEvent.getState());
System.out.println("收到事件所发生节点的类型" + watchedEvent.getType());

}
}
});
}

@Test
public void testGetWatch() throws Exception {
byte[] data = zk.getData("/mygirls",true, null);
List children = zk.getChildren("/mygirls", true);
System.out.println(new String(data,"UTF-8"));
Thread.sleep(Long.MAX_VALUE);
}
}

ZooKeeper开发分布式系统案例代码,动态上下线感知。

服务代码:
public class TimeQueryServer {
ZooKeeper zk=null;
public void connectZk()throws Exception{
zk=new ZooKeeper("hdp-01:2181,hdp-02:2181,hdp-03:2181", 2000, null);
}

public void registerServerInfo(String hostname,String port)throws Exception{
/**
* 先判断注册节点的父节点是否存在,如果不存在,则创建持久节点
*/
Stat exists = zk.exists("/servers", false);
if(exists==null){
zk.create("/servers",null,ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.PERSISTENT);
}
/**
* 注册服务器数据到zk的约定注册节点下
*/
String create = zk.create("/servers/server", (hostname + ":" + port).getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println(hostname+" 服务器向zk 注册成功,注册节点为:/servers"+create);
}
public static void main(String[] args) throws Exception {
//1.构造zk连接
TimeQueryServer timeQueryServer = new TimeQueryServer();
timeQueryServer.connectZk();
//2.注册服务器信息
timeQueryServer.registerServerInfo("192.168.150.3","44772");
//3.启动业务线程开始处理业务
new TimeQueryService(44772).start();
}
}

public class TimeQueryService extends Thread {
int port=0;
public TimeQueryService(int port){
this.port=port;
}
@Override
public void run() {
try {
ServerSocket ss = new ServerSocket(port);
System.out.println("业务线程已经绑定端口"+port+"开始接受客户端请求..");
while (true){
Socket sc = ss.accept();
InputStream inputStream = sc.getInputStream();
OutputStream outputStream = sc.getOutputStream();
outputStream.write(new Date().toString().getBytes());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

消费者代码:
public class Consumer {

//定义一个list用于存放在线的服务器列表
private volatile ArrayListonlineServers=new ArrayList();
ZooKeeper zk=null;
public void connectZk()throws Exception{
zk=new ZooKeeper("hdp-01:2181,hdp-02:2181,hdp-03:2181", 2000, new Watcher() {
public void process(WatchedEvent watchedEvent) {
if (watchedEvent.getState()==Event.KeeperState.SyncConnected && watchedEvent.getType()==Event.EventType.NodeChildrenChanged){
try{
//事件回调逻辑中,再次查询zk上在线服务器节点即可,查询逻辑中又再次注册子节点变化事件监听
getOnlineServers();
}catch (Exception e){
e.printStackTrace();
}
}
}
});
}
//查询在线服务器列表
public void getOnlineServers()throws Exception{
List children = zk.getChildren("/servers", true);
ArrayList servers = new ArrayList();
for (String child:children){
byte[] data = zk.getData("/servers/" + child, false, null);
String serverInfo=new String(data);
servers.add(serverInfo);
}
onlineServers=servers;
System.out.println("查询了一次zk,当前在线的服务器有:"+servers);

}

public void setRequest() throws Exception {
Random random = new Random();
while (true){
try {
int nextInt=random.nextInt(onlineServers.size());
String server=onlineServers.get(nextInt);
String hostname=server.split(":")[0];
int port=Integer.parseInt(server.split(":")[1]);
System.out.println("本次请求挑选的服务器为:"+server);

Socket socket = new Socket(hostname, port);
OutputStream out = socket.getOutputStream();
InputStream in = socket.getInputStream();
out.write("hahaha".getBytes());
out.flush();

byte[] buf = new byte[256];
int read=in.read(buf);
String s = new String(buf, 0, read);
System.out.println("服务器响应时间为:"+s);
out.close();
in.close();
socket.close();
Thread.sleep(2000);
}catch (Exception e){

}

}
}
public static void main(String[] args) throws Exception {
//构造zk连接对象
Consumer consumer = new Consumer();
consumer.connectZk();
//查询在线服务器列表
consumer.getOnlineServers();
//处理业务
consumer.setRequest();
}
}

pom


junit
junit
RELEASE


org.apache.logging.log4j
log4j-core
2.8.2



org.apache.zookeeper
zookeeper
3.4.10


启动多个服务。

控制台输出:

192.168.150.3 服务器向zk 注册成功,注册节点为:/servers/servers/server0000000018
业务线程已经绑定端口44772开始接受客户端请求..

192.168.150.3 服务器向zk 注册成功,注册节点为:/servers/servers/server0000000019
业务线程已经绑定端口44773开始接受客户端请求..

192.168.150.3 服务器向zk 注册成功,注册节点为:/servers/servers/server0000000020
业务线程已经绑定端口44774开始接受客户端请求..

消费者启动

控制台输出:

查询了一次zk,当前在线的服务器有:[192.168.150.3:44773, 192.168.150.3:44772, 192.168.150.3:44774]
本次请求挑选的服务器为:192.168.150.3:44772
服务器响应时间为:Mon Jun 03 20:03:21 CST 2019
本次请求挑选的服务器为:192.168.150.3:44773
服务器响应时间为:Mon Jun 03 20:03:23 CST 2019
本次请求挑选的服务器为:192.168.150.3:44773
服务器响应时间为:Mon Jun 03 20:03:25 CST 2019
本次请求挑选的服务器为:192.168.150.3:44772
服务器响应时间为:Mon Jun 03 20:03:27 CST 2019

下线一个服务后,控制台输出:

查询了一次zk,当前在线的服务器有:[192.168.150.3:44773, 192.168.150.3:44772]
本次请求挑选的服务器为:192.168.150.3:44773
服务器响应时间为:Mon Jun 03 20:04:19 CST 2019
本次请求挑选的服务器为:192.168.150.3:44773
服务器响应时间为:Mon Jun 03 20:04:21 CST 2019
本次请求挑选的服务器为:192.168.150.3:44773
服务器响应时间为:Mon Jun 03 20:04:23 CST 2019
本次请求挑选的服务器为:192.168.150.3:44773

原文链接:https://my.oschina.net/u/3995125/blog/3057475