Quantcast
Channel: IT社区推荐资讯 - ITIndex.net
Viewing all 15905 articles
Browse latest View live

HBase统计表行数(RowCount)的四种方法_Abysscarry的博客-CSDN博客

$
0
0

背景:
对于其他数据存储系统来说,统计表的行数是再基本不过的操作了,一般实现都非常简单;但对于HBase这种key-value存储结构的列式数据库,统计 RowCount的方法却有好几种不同的花样,并且 执行效率差别巨大!下面来研究下吧~


测试集群:HBase1.2.0 - CDH5.13.0 四台服务器

注:以下4种方法效率依次提高


一、hbase-shell的count命令

这是最简单直接的操作,但是 执行效率非常低,适用于 百万级以下的小表RowCount统计!
在这里插入图片描述

hbase> count 'ns1:t1'
 hbase> count 't1'
 hbase> count 't1', INTERVAL => 100000
 hbase> count 't1', CACHE => 1000
 hbase> count 't1', INTERVAL => 10, CACHE => 1000

此操作可能需要很长时间,来运行计数MapReduce作业。默认情况下每1000行显示当前计数,计数间隔可自行指定。

默认情况下在计数扫描上启用缓存,默认缓存大小为10行。

行数为 3000W 的表测试结果:

hbase(main):001:0>count'sda_crm_calls20180102'

在这里插入图片描述
默认INTERVAL为1000行时花了80分钟。。

hbase(main):001:0>count'sda_crm_calls20180102',INTERVAL=>1000000

在这里插入图片描述

INTERVAL为1000000行时花了130分钟。。


二、scan方式设置过滤器循环计数(JAVA实现)

这种方式是通过添加 FirstKeyOnlyFilter过滤器的scan进行全表扫描,循环计数RowCount, 速度较慢!但快于第一种count方式!

基本代码如下:

public void rowCountByScanFilter(String tablename){
    long rowCount = 0;
    try {
        //计时
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        TableName name=TableName.valueOf(tablename);
        //connection为类静态变量
        Table table = connection.getTable(name);
        Scan scan = new Scan();
        //FirstKeyOnlyFilter只会取得每行数据的第一个kv,提高count速度
        scan.setFilter(new FirstKeyOnlyFilter());
        
        ResultScanner rs = table.getScanner(scan);
        for (Result result : rs) {
            rowCount += result.size();
        }

        stopWatch.stop();
        System.out.println("RowCount: " + rowCount);
        System.out.println("统计耗时:" +stopWatch.getTotalTimeMillis());
    } catch (Throwable e) {
        e.printStackTrace();
    }
}

在这里插入图片描述

耗时45分钟!


三、利用hbase.RowCounter包执行MR任务

这种方式 效率非常高!利用了hbase jar中自带的统计行数的工具类!

通过 $HBASE_HOME/bin/hbase命令执行:

[root@cdh1~]# hbase org.apache.hadoop.hbase.mapreduce.RowCounter'sda_crm_calls20180102'

在这里插入图片描述
在这里插入图片描述

耗时 1m40s,速度较上面两种有了质的飞跃!


四、利用HBase协处理器Coprocessor(JAVA实现)

这是我目前发现 效率最高的RowCount统计方式,利用了HBase高级特性: 协处理器

我们往往使用过滤器来减少服务器端通过网络返回到客户端的数据量。但HBase中还有一些特性让用户甚至可以 把一部分计算也移动到数据的存放端,那就是协处理器 (coprocessor)。

协处理器简介:

(节选自《HBase权威指南》)

使用客户端API,配合筛选机制,例如,使用过滤器或限制列族的范围,都可以控制被返回到客户端的数据量。如果可以更进一步优化会更好,例如, 数据的处理流程直接放到服务器端执行,然后仅返回一个小的处理结果集。这类似于一个小型的MapReduce框架,该框架将工作分发到整个集群。

协处理器 允许用户在region服务器上运行自己的代码,更准确地说是 允许用户执行region级的操作,并且可以使用与RDBMS中触发器(trigger)类似的功能。在客户端,用户不用关心操作具体在哪里执行,HBase的分布式框架会帮助用户把这些工作变得透明。

实现代码:

public void rowCountByCoprocessor(String tablename){
    try {
        //提前创建connection和conf
        Admin admin = connection.getAdmin();
        TableName name=TableName.valueOf(tablename);
        //先disable表,添加协处理器后再enable表
        admin.disableTable(name);
        HTableDescriptor descriptor = admin.getTableDescriptor(name);
        String coprocessorClass = "org.apache.hadoop.hbase.coprocessor.AggregateImplementation";
        if (! descriptor.hasCoprocessor(coprocessorClass)) {
            descriptor.addCoprocessor(coprocessorClass);
        }
        admin.modifyTable(name, descriptor);
        admin.enableTable(name);

        //计时
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        Scan scan = new Scan();
        AggregationClient aggregationClient = new AggregationClient(conf);

        System.out.println("RowCount: " + aggregationClient.rowCount(name, new LongColumnInterpreter(), scan));
        stopWatch.stop();
        System.out.println("统计耗时:" +stopWatch.getTotalTimeMillis());
    } catch (Throwable e) {
        e.printStackTrace();
    }
}

在这里插入图片描述
发现只花了 23秒 就统计完成!

为什么利用协处理器后速度会如此之快?

Table注册了Coprocessor之后,在执行AggregationClient的时候,会将RowCount分散到Table的每一个Region上,Region内RowCount的计算,是通过RPC执行调用接口,由Region对应的RegionServer执行InternalScanner进行的。

因此,性能的提升有两点原因:

1. 分布式统计。将原来客户端按照Rowkey的范围单点进行扫描,然后统计的方式,换成了由所有Region 所在RegionServer同时计算的过程。

2.使用了在RegionServer内部执行使用了 InternalScanner。这是距离实际存储最近的Scanner接口,存取更加快捷。


手把手教你如何用10元快速买入BTC

$
0
0

比特币突破1万美元,大牛市开启 

比特币是由一串世界上独一无二的数字串组成,人们挖矿交易的比特币,都是在交换这些数字串,它是无法触摸的又是真实存在的。

比特币估值逐年攀升。第一枚比特币诞生于2009年,当时比特币一文不名,仅仅在少数科技极客之间流转。随着各国对比特币接受度越来越广泛,比特币价格最高涨至1.7万美元,涨幅高达300万倍。2020新年,仅仅两个月,比特币最高涨幅已达39%倍。 

根据华尔街日报最近的一份报告显示,到目前为止,比特币给予持有者的回报远高于原油、标准普尔500指数等。

随着产量减半日期逼近,比特币价格再次突破10000美元大关,币圈将再次上演牛市行情,加密货币分析师预测比特币今年5月产品减半前后有望突破20000万美元大关,创下历史新高。

需要强调的是,比特币是合法财产。广东省法院一份通知书明确的指出:“比特币虽然是一种网络虚拟商品,但具有可转化为现实物质利益的属性,在法律属性上认定为财产。”

如何用10元购买比特币?

那么如何拥有人生第一枚比特币呢?选择一个靠谱的交易所很重要,今天就以头部交易平台——OKEx为例,手把手教大家如何买入人生的第一枚比特币。

在OKEx比特币10元起购,假如你只是想购买0.0001枚比特币,OKEx也是支持的。

第一步:下载APP/客户端

首先,在微博上搜索OKEx,并关注OKEx的官方微博账号。在OKEx微博主页点击APP/PC客户端的下载入口,获得下载链接。

作为业内一线交易所,你在OKEx不仅可以用法币买到BTC,还能进行LTC、ETH、EOS 等众多数字资产的法币、币币、杠杆、合约等交易。

根据OKEx的的平台政策,如果你持有OKEx的平台币,即OKB(可通过法币交易快速买入),还能享受交易手续费折扣优惠,最高可以享受6折优惠,力度非常之大。 

同时,OKEx平台采用了严格地KYC审核机制、国际顶级风控技术、专职安全人员24H值班制度等严格保障你出入金的安全。

第二步:一键买币 

接下来的步骤就比较简单了,你只要按照提示完成注册、认证流程,在法币交易区选择任一“商家”,按照下图页面流程显示,即可完成购买:

最后提醒,数字货币是一种高风险高收益的投资资产,请慎重考虑本人风险承受能力,并对比特币有充分了解之后,再做买入,注意综合评估。

 

雷锋网雷锋网雷锋网 

指数基金速查表,轻松找到对的基!

$
0
0

2019年是指数基金大爆发的一年,产品数量和种类都大幅增加,基金规模也随之增长。当然,更重要的是指数基金的投资者也越来越多,以往这个不受个人投资者重视的品类,开始得到越来越多投资者的认可。

柠檬君整理了一些大家在投资中可能用得上的产品,制作了两个表格分享给大家。

第一张表是场内指数基金表,主要包括市场上交易量和基金规模相对领先的ETF和个别LOF。由于2019年ETF市场蓬勃发展,入围产品的交易量门槛大幅提高,不是日均千万级别的产品就很难有机会入围。在场内买基金的时候,大家可以优先考虑这些产品。指数产品同质化是非常严重的,ETF最重要的还是流动性,毕竟是交易型产品,如果持有时间比较长,可以适当放宽这个要求,更侧重于产品的收益能力。

ETF的价格战是一个热门话题,但是一味的价格战对于行业的发展并不见得是一件好事,作为投资者虽然乐见降费,但是也要居安思危。交易型产品,永远是流动性放在第一位,降低费率只是手段,最终目的是靠低费率吸引资金,做大规模提升流动性,否则一切的努力都是徒劳的。我们付费是为了得到勤勉尽职的服务。

第二张表是场外指数基金表,柠檬君精选了部分指数基金,这么大一张表格还是可以做到品种齐全,数量丰富的,想买什么指数基金基本在这里都能找得到对应标的。备注里有C的代表该基金有C类份额,适合短线交易,沪深300和中证500对应的产品除了天弘的两只都是增强型指数基金,名字里看不出是增强型的易方达上证50也特别标注出来了,备注里有等权的代表该指数是等权重指数。前述的场内基金,比如ETF会有联接基金,LOF也可以场外申赎,多数就没有重复列在表二中。

场外指数基金的受众更为广大,很多投资者受限于种种因素,使用证券账户场内交易并不方便,还是依赖于场外指数基金的:

在这个市场上,广发基金的布局是最值得关注的,产品线优势明显,尤其是在行业/主题指数基金的布局上,广发基金占据绝对优势,很多指数基金都是市场上独一份的,比如家电、建筑材料、汽车、工业。在各类型指数基金里,行业指数基金发展是最困难的,最难做出规模,敢于在这个领域投入,勇气可嘉。正是有了这样的基金公司,投资者也才有了更丰富的指数基金可以选择。日后复工,如果产业链中下游复工速度快于上游,对于原材料的迫切需求将会推动原材料价格上涨,广发中证建筑材料这样的基金就将受益。

策略指数基金,去年日子很不好过,还是之前说过的,产品研发似乎缺少一些预见性,这也是这些年指数基金领域一个很大的误区。

宽基指数基金,攻守激烈,都想去分别人一杯羹,也都想守住自己的一亩三分地。MSCI系列指数基金的激战也没有什么成效,想在这里做出点成绩也是比较难的,MSCI还是要背锅的,对于普通投资者来说,海外指数公司的行情和资料,都是很难获取的,没法进行一个全面深入的了解,基金公司给的数据往往是美化过的,根本没法看,不熟不做,也就很难有什么发展了,标普、富时罗素什么的,也很难,有的基金公司过度美化数据反倒坑了指数公司的口碑……

@今日话题



本话题在雪球有12条讨论,点击查看。
雪球是一个投资者的社交网络,聪明的投资者都在这里。
点击下载雪球手机客户端 http://xueqiu.com/xz

JHipster生成微服务架构的应用栈(一)- 准备工作 - 羽客 - 博客园

$
0
0

本系列文章演示如何用JHipster生成一个微服务架构风格的应用栈。
环境需求:安装好JHipster开发环境的CentOS 7.4( 参考这里
应用栈名称:appstack
认证微服务: uaa
业务微服务:microservice1
网关微服务:gateway
实体名:role
主机IP:192.168.220.120

微服务体系规划

本系列文章会说明如何生成uaa(即图中的JHipster UAA),microservice1,gateway这3个微服务。
JHipster Console是现有的轮子,比较复杂,会有单独文章来介绍。
JHipster Registry也是现有的轮子,这里直接下载一个镜像来使用。

安装Docker

推荐版本:17.06
完整安装说明,请 参考这里

启动一个JHipster Registry

在命令行,任意目录下,启动一个JHipster Registry容器;如果本地没有jhipster/jhipster-registry:v4.0.0的镜像,容器启动时会自动去docker store下载镜像。

docker container run --name registry-app -e JHIPSTER.SECURITY.AUTHENTICATION.JWT.SECRET=dkk20dldkf0209342334 -d -p 8761:8761 jhipster/jhipster-registry:v4.0.0

启动完成后,可以通过浏览器访问 http://192.168.220.120:8761,登录名和密码默认都是 admin

可以看到在 Instances Registered区域,还没有注册的微服务。

创建整个应用栈的目录结构

在命令行,根据 微服务体系规划,创建一个目录结构:

-- appstack
  |-- uaa
  |-- microservice1
  |-- gateway

系列文章

JHipster生成微服务架构的应用栈(一)- 准备工作
JHipster生成微服务架构的应用栈(二)- 认证微服务示例
JHipster生成微服务架构的应用栈(三)- 业务微服务示例
JHipster生成微服务架构的应用栈(四)- 网关微服务示例
JHipster生成微服务架构的应用栈(五)- 容器编排示例

Kubelet 中的 “PLEG is not healthy” 到底是个什么鬼?

$
0
0

原文链接: 深入理解 Kubelet 中的 PLEG is not healthy

在 Kubernetes 社区中, PLEG is not healthy成名已久,只要出现这个报错,就有很大概率造成 Node 状态变成 NotReady。社区相关的 issue 也有一大把,先列几个给你们看看:

本文我将尝试解释 PLEG 的工作原理,只要理解了工作原理,再遇到类似的问题就有排查思路了。

1. PLEG 是个啥?

PLEG 全称叫 Pod Lifecycle Event Generator,即 Pod 生命周期事件生成器。实际上它只是 Kubelet中的一个模块,主要职责就是通过每个匹配的 Pod 级别事件来调整容器运行时的状态,并将调整的结果写入缓存,使 Pod的缓存保持最新状态。先来聊聊 PLEG 的出现背景。

在 Kubernetes 中,每个节点上都运行着一个守护进程 Kubelet来管理节点上的容器,调整容器的实际状态以匹配 spec中定义的状态。具体来说,Kubelet 需要对两个地方的更改做出及时的回应:

  1. Pod spec 中定义的状态
  2. 容器运行时的状态

对于 Pod,Kubelet 会从多个数据来源 watch Pod spec 中的变化。对于容器,Kubelet 会定期(例如,10s)轮询容器运行时,以获取所有容器的最新状态。

随着 Pod 和容器数量的增加,轮询会产生不可忽略的开销,并且会由于 Kubelet 的并行操作而加剧这种开销(为每个 Pod 分配一个 goruntine,用来获取容器的状态)。轮询带来的周期性大量并发请求会导致较高的 CPU 使用率峰值(即使 Pod 的定义和容器的状态没有发生改变),降低性能。最后容器运行时可能不堪重负,从而降低系统的可靠性,限制 Kubelet 的可扩展性。

为了降低 Pod 的管理开销,提升 Kubelet 的性能和可扩展性,引入了 PLEG,改进了之前的工作方式:

  • 减少空闲期间的不必要工作(例如 Pod 的定义和容器的状态没有发生更改)。
  • 减少获取容器状态的并发请求数量。

整体的工作流程如下图所示,虚线部分是 PLEG 的工作内容。

2. PLEG is not healthy 是如何发生的?

Healthy()函数会以 “PLEG” 的形式添加到 runtimeState中,Kubelet 在一个同步循环( SyncLoop()函数)中会定期(默认是 10s)调用 Healthy()函数。 Healthy()函数会检查 relist进程(PLEG 的关键任务)是否在 3 分钟内完成。如果 relist 进程的完成时间超过了 3 分钟,就会报告 PLEG is not healthy

我会在流程的每一步通过源代码解释其相关的工作原理,源代码基于 Kubernetes 1.11(Openshift 3.11)。如果你不熟悉 Go 的语法也不用担心,只需要看代码中的注释就能明白其原理。我也会在放出代码之前先解读一番,并从源代码中裁剪掉不太重要的内容以提高代码的可读性。下面是调用 healthy() 函数的相关代码:

//// pkg/kubelet/pleg/generic.go - Healthy()

// The threshold needs to be greater than the relisting period + the
// relisting time, which can vary significantly. Set a conservative
// threshold to avoid flipping between healthy and unhealthy.
relistThreshold = 3 * time.Minute
:
func (g *GenericPLEG) Healthy() (bool, error) {
  relistTime := g.getRelistTime()
  elapsed := g.clock.Since(relistTime)
  if elapsed > relistThreshold {
    return false, fmt.Errorf("pleg was last seen active %v ago; threshold is %v", elapsed, relistThreshold)
  }
  return true, nil
}

//// pkg/kubelet/kubelet.go - NewMainKubelet()
func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration, ...
:
  klet.runtimeState.addHealthCheck("PLEG", klet.pleg.Healthy)

//// pkg/kubelet/kubelet.go - syncLoop()
func (kl *Kubelet) syncLoop(updates <-chan kubetypes.PodUpdate, handler SyncHandler) {
:
// The resyncTicker wakes up kubelet to checks if there are any pod workers
// that need to be sync'd. A one-second period is sufficient because the
// sync interval is defaulted to 10s.
:
  const (
    base   = 100 * time.Millisecond
    max    = 5 * time.Second
    factor = 2
  )
  duration := base
  for {
      if rs := kl.runtimeState.runtimeErrors(); len(rs) != 0 {
          glog.Infof("skipping pod synchronization - %v", rs)
          // exponential backoff
          time.Sleep(duration)
          duration = time.Duration(math.Min(float64(max), factor*float64(duration)))
          continue
      }
    :
  }
:
}

//// pkg/kubelet/runtime.go - runtimeErrors()
func (s *runtimeState) runtimeErrors() []string {
:
    for _, hc := range s.healthChecks {
        if ok, err := hc.fn(); !ok {
            ret = append(ret, fmt.Sprintf("%s is not healthy: %v", hc.name, err))
        }
    }
:
}
复制代码

3. 深入解读 relist 函数

上文提到 healthy()函数会检查 relist 的完成时间,但 relist 究竟是用来干嘛的呢?解释 relist 之前,要先解释一下 Pod 的生命周期事件。Pod 的生命周期事件是在 Pod 层面上对底层容器状态改变的抽象,使其与底层的容器运行时无关,这样就可以让 Kubelet 不受底层容器运行时的影响。

type PodLifeCycleEventType string

const (
    ContainerStarted      PodLifeCycleEventType = "ContainerStarted"
    ContainerStopped      PodLifeCycleEventType = "ContainerStopped"
    NetworkSetupCompleted PodLifeCycleEventType = "NetworkSetupCompleted"
    NetworkFailed         PodLifeCycleEventType = "NetworkFailed"
)

// PodLifecycleEvent is an event reflects the change of the pod state.
type PodLifecycleEvent struct {
    // The pod ID.
    ID types.UID
    // The type of the event.
    Type PodLifeCycleEventType
    // The accompanied data which varies based on the event type.
    Data interface{}
}
复制代码

以 Docker 为例,在 Pod 中启动一个 infra 容器就会在 Kubelet 中注册一个 NetworkSetupCompleted Pod 生命周期事件。

那么 PLEG 是如何知道新启动了一个 infra 容器呢?它会定期重新列出节点上的所有容器(例如 docker ps),并与上一次的容器列表进行对比,以此来判断容器状态的变化。其实这就是 relist()函数干的事情,尽管这种方法和以前的 Kubelet 轮询类似,但现在只有一个线程,就是 PLEG。现在不需要所有的线程并发获取容器的状态,只有相关的线程会被唤醒用来同步容器状态。而且 relist 与容器运行时无关,也不需要外部依赖,简直完美。

下面我们来看一下 relist()函数的内部实现。完整的流程如下图所示:

注意图中的 RPC 调用部分,后文将会拎出来详细解读。完整的源代码在 这里

尽管每秒钟调用一次 relist,但它的完成时间仍然有可能超过 1s。因为下一次调用 relist必须得等上一次 relist 执行结束,设想一下,如果容器运行时响应缓慢,或者一个周期内有大量的容器状态发生改变,那么 relist的完成时间将不可忽略,假设是 5s,那么下一次调用 relist将要等到 6s 之后。

相关的源代码如下:

//// pkg/kubelet/kubelet.go - NewMainKubelet()

// Generic PLEG relies on relisting for discovering container events.
// A longer period means that kubelet will take longer to detect container
// changes and to update pod status. On the other hand, a shorter period
// will cause more frequent relisting (e.g., container runtime operations),
// leading to higher cpu usage.
// Note that even though we set the period to 1s, the relisting itself can
// take more than 1s to finish if the container runtime responds slowly
// and/or when there are many container changes in one cycle.
plegRelistPeriod = time.Second * 1

// NewMainKubelet instantiates a new Kubelet object along with all the required internal modules.
// No initialization of Kubelet and its modules should happen here.
func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration, ...
:
  klet.pleg = pleg.NewGenericPLEG(klet.containerRuntime, plegChannelCapacity, plegRelistPeriod, klet.podCache, clock.RealClock{})

//// pkg/kubelet/pleg/generic.go - Start()

// Start spawns a goroutine to relist periodically.
func (g *GenericPLEG) Start() {
  go wait.Until(g.relist, g.relistPeriod, wait.NeverStop)
}

//// pkg/kubelet/pleg/generic.go - relist()
func (g *GenericPLEG) relist() {
... WE WILL REVIEW HERE ...
}
复制代码

回到上面那幅图,relist 函数第一步就是记录 Kubelet的相关指标(例如 kubelet_pleg_relist_latency_microseconds),然后通过 CRI 从容器运行时获取当前的 Pod 列表(包括停止的 Pod)。该 Pod 列表会和之前的 Pod 列表进行比较,检查哪些状态发生了变化,然后同时生成相关的 Pod 生命周期事件更改后的状态

//// pkg/kubelet/pleg/generic.go - relist()
  :
  // get a current timestamp
  timestamp := g.clock.Now()

  // kubelet_pleg_relist_latency_microseconds for prometheus metrics
    defer func() {
        metrics.PLEGRelistLatency.Observe(metrics.SinceInMicroseconds(timestamp))
    }()

  // Get all the pods.
    podList, err := g.runtime.GetPods(true)
  :
复制代码

其中 GetPods()函数的调用堆栈如下图所示:

相关的源代码如下:

//// pkg/kubelet/kuberuntime/kuberuntime_manager.go - GetPods()

// GetPods returns a list of containers grouped by pods. The boolean parameter
// specifies whether the runtime returns all containers including those already
// exited and dead containers (used for garbage collection).
func (m *kubeGenericRuntimeManager) GetPods(all bool) ([]*kubecontainer.Pod, error) {
    pods := make(map[kubetypes.UID]*kubecontainer.Pod)
    sandboxes, err := m.getKubeletSandboxes(all)
:
}

//// pkg/kubelet/kuberuntime/kuberuntime_sandbox.go - getKubeletSandboxes()

// getKubeletSandboxes lists all (or just the running) sandboxes managed by kubelet.
func (m *kubeGenericRuntimeManager) getKubeletSandboxes(all bool) ([]*runtimeapi.PodSandbox, error) {
:
    resp, err := m.runtimeService.ListPodSandbox(filter)
:
}

//// pkg/kubelet/remote/remote_runtime.go - ListPodSandbox()

// ListPodSandbox returns a list of PodSandboxes.
func (r *RemoteRuntimeService) ListPodSandbox(filter *runtimeapi.PodSandboxFilter) ([]*runtimeapi.PodSandbox, error) {
:
    resp, err := r.runtimeClient.ListPodSandbox(ctx, &runtimeapi.ListPodSandboxRequest{
:
    return resp.Items, nil
}
复制代码

获取所有的 Pod 列表后, relist的完成时间就会更新成当前的时间戳。也就是说, Healthy()函数可以根据这个时间戳来评估 relist 是否超过了 3 分钟。

//// pkg/kubelet/pleg/generic.go - relist()

  // update as a current timestamp
  g.updateRelistTime(timestamp)
复制代码

将当前的 Pod 列表和上一次 relist 的 Pod 列表进行对比之后,就会针对每一个变化生成相应的 Pod 级别的事件。相关的源代码如下:

//// pkg/kubelet/pleg/generic.go - relist()

  pods := kubecontainer.Pods(podList)
  g.podRecords.setCurrent(pods)

  // Compare the old and the current pods, and generate events.
  eventsByPodID := map[types.UID][]*PodLifecycleEvent{}
  for pid := range g.podRecords {
    oldPod := g.podRecords.getOld(pid)
    pod := g.podRecords.getCurrent(pid)

    // Get all containers in the old and the new pod.
    allContainers := getContainersFromPods(oldPod, pod)
    for _, container := range allContainers {
          events := computeEvents(oldPod, pod, &container.ID)

          for _, e := range events {
                updateEvents(eventsByPodID, e)
          }
        }
  }
复制代码

其中 generateEvents()函数( computeEvents()函数会调用它)用来生成相应的 Pod 级别的事件(例如 ContainerStartedContainerDied等等),然后通过 updateEvents()函数来更新事件。

computeEvents()函数的内容如下:

//// pkg/kubelet/pleg/generic.go - computeEvents()

func computeEvents(oldPod, newPod *kubecontainer.Pod, cid *kubecontainer.ContainerID) []*PodLifecycleEvent {
:
    return generateEvents(pid, cid.ID, oldState, newState)
}

//// pkg/kubelet/pleg/generic.go - generateEvents()

func generateEvents(podID types.UID, cid string, oldState, newState plegContainerState) []*PodLifecycleEvent {
:
    glog.V(4).Infof("GenericPLEG: %v/%v: %v -> %v", podID, cid, oldState, newState)
    switch newState {
    case plegContainerRunning:
      return []*PodLifecycleEvent{{ID: podID, Type: ContainerStarted, Data: cid}}
    case plegContainerExited:
      return []*PodLifecycleEvent{{ID: podID, Type: ContainerDied, Data: cid}}
    case plegContainerUnknown:
      return []*PodLifecycleEvent{{ID: podID, Type: ContainerChanged, Data: cid}}
    case plegContainerNonExistent:
      switch oldState {
      case plegContainerExited:
        // We already reported that the container died before.
        return []*PodLifecycleEvent{{ID: podID, Type: ContainerRemoved, Data: cid}}
      default:
        return []*PodLifecycleEvent{{ID: podID, Type: ContainerDied, Data: cid}, {ID: podID, Type: ContainerRemoved, Data: cid}}
      }
    default:
      panic(fmt.Sprintf("unrecognized container state: %v", newState))
  }
}
复制代码

relist 的最后一个任务是检查是否有与 Pod 关联的事件,并按照下面的流程更新 podCache

//// pkg/kubelet/pleg/generic.go - relist()

  // If there are events associated with a pod, we should update the
  // podCache.
  for pid, events := range eventsByPodID {
    pod := g.podRecords.getCurrent(pid)
    if g.cacheEnabled() {
      // updateCache() will inspect the pod and update the cache. If an
      // error occurs during the inspection, we want PLEG to retry again
      // in the next relist. To achieve this, we do not update the
      // associated podRecord of the pod, so that the change will be
      // detect again in the next relist.
      // TODO: If many pods changed during the same relist period,
      // inspecting the pod and getting the PodStatus to update the cache
      // serially may take a while. We should be aware of this and
      // parallelize if needed.
      if err := g.updateCache(pod, pid); err != nil {
        glog.Errorf("PLEG: Ignoring events for pod %s/%s: %v", pod.Name, pod.Namespace, err)
        :
      }
      :
    }
    // Update the internal storage and send out the events.
    g.podRecords.update(pid)
    for i := range events {
      // Filter out events that are not reliable and no other components use yet.
      if events[i].Type == ContainerChanged {
           continue
      }
      g.eventChannel <- events[i]
     }
  }
复制代码

updateCache()将会检查每个 Pod,并在单个循环中依次对其进行更新。因此,如果在同一个 relist 中更改了大量的 Pod,那么 updateCache 过程将会成为瓶颈。最后,更新后的 Pod 生命周期事件将会被发送到 eventChannel

某些远程客户端还会调用每一个 Pod 来获取 Pod 的 spec 定义信息,这样一来,Pod 数量越多,延时就可能越高,因为 Pod 越多就会生成越多的事件。

updateCache()的详细调用堆栈如下图所示,其中 GetPodStatus()用来获取 Pod 的 spec 定义信息:

完整的代码如下:

//// pkg/kubelet/pleg/generic.go - updateCache()

func (g *GenericPLEG) updateCache(pod *kubecontainer.Pod, pid types.UID) error {
:
    timestamp := g.clock.Now()
    // TODO: Consider adding a new runtime method
    // GetPodStatus(pod *kubecontainer.Pod) so that Docker can avoid listing
    // all containers again.
    status, err := g.runtime.GetPodStatus(pod.ID, pod.Name, pod.Namespace)
  :
    g.cache.Set(pod.ID, status, err, timestamp)
    return err
}

//// pkg/kubelet/kuberuntime/kuberuntime_manager.go - GetPodStatus()

// GetPodStatus retrieves the status of the pod, including the
// information of all containers in the pod that are visible in Runtime.
func (m *kubeGenericRuntimeManager) GetPodStatus(uid kubetypes.UID, name, namespace string) (*kubecontainer.PodStatus, error) {
  podSandboxIDs, err := m.getSandboxIDByPodUID(uid, nil)
  :
    for idx, podSandboxID := range podSandboxIDs {
        podSandboxStatus, err := m.runtimeService.PodSandboxStatus(podSandboxID)
    :
    }

    // Get statuses of all containers visible in the pod.
    containerStatuses, err := m.getPodContainerStatuses(uid, name, namespace)
  :
}

//// pkg/kubelet/kuberuntime/kuberuntime_sandbox.go - getSandboxIDByPodUID()

// getPodSandboxID gets the sandbox id by podUID and returns ([]sandboxID, error).
// Param state could be nil in order to get all sandboxes belonging to same pod.
func (m *kubeGenericRuntimeManager) getSandboxIDByPodUID(podUID kubetypes.UID, state *runtimeapi.PodSandboxState) ([]string, error) {
  :
  sandboxes, err := m.runtimeService.ListPodSandbox(filter)
  :  
  return sandboxIDs, nil
}

//// pkg/kubelet/remote/remote_runtime.go - PodSandboxStatus()

// PodSandboxStatus returns the status of the PodSandbox.
func (r *RemoteRuntimeService) PodSandboxStatus(podSandBoxID string) (*runtimeapi.PodSandboxStatus, error) {
    ctx, cancel := getContextWithTimeout(r.timeout)
    defer cancel()

    resp, err := r.runtimeClient.PodSandboxStatus(ctx, &runtimeapi.PodSandboxStatusRequest{
        PodSandboxId: podSandBoxID,
    })
  :
    return resp.Status, nil
}

//// pkg/kubelet/kuberuntime/kuberuntime_container.go - getPodContainerStatuses()

// getPodContainerStatuses gets all containers' statuses for the pod.
func (m *kubeGenericRuntimeManager) getPodContainerStatuses(uid kubetypes.UID, name, namespace string) ([]*kubecontainer.ContainerStatus, error) {
  // Select all containers of the given pod.
  containers, err := m.runtimeService.ListContainers(&runtimeapi.ContainerFilter{
    LabelSelector: map[string]string{types.KubernetesPodUIDLabel: string(uid)},
  })
  :
  // TODO: optimization: set maximum number of containers per container name to examine.
  for i, c := range containers {
    status, err := m.runtimeService.ContainerStatus(c.Id)
    :
  }
  :
  return statuses, nil
}
复制代码

上面就是 relist() 函数的完整调用堆栈,我在讲解的过程中结合了相关的源代码,希望能为你提供有关 PLEG 的更多细节。为了实时了解 PLEG 的健康状况,最好的办法就是监控 relist。

4. 监控 relist

我们可以通过监控 Kubelet 的指标来了解 relist的延时。 relist的调用周期是 1s,那么 relist 的完成时间 + 1s就等于 kubelet_pleg_relist_interval_microseconds指标的值。你也可以监控容器运行时每个操作的延时,这些指标在排查故障时都能提供线索。

你可以在每个节点上通过访问 URL https://127.0.0.1:10250/metrics来获取 Kubelet 的指标。

# HELP kubelet_pleg_relist_interval_microseconds Interval in microseconds between relisting in PLEG.
# TYPE kubelet_pleg_relist_interval_microseconds summary
kubelet_pleg_relist_interval_microseconds{quantile="0.5"} 1.054052e+06
kubelet_pleg_relist_interval_microseconds{quantile="0.9"} 1.074873e+06
kubelet_pleg_relist_interval_microseconds{quantile="0.99"} 1.126039e+06
kubelet_pleg_relist_interval_microseconds_count 5146

# HELP kubelet_pleg_relist_latency_microseconds Latency in microseconds for relisting pods in PLEG.
# TYPE kubelet_pleg_relist_latency_microseconds summary
kubelet_pleg_relist_latency_microseconds{quantile="0.5"} 53438
kubelet_pleg_relist_latency_microseconds{quantile="0.9"} 74396
kubelet_pleg_relist_latency_microseconds{quantile="0.99"} 115232
kubelet_pleg_relist_latency_microseconds_count 5106

# HELP kubelet_runtime_operations Cumulative number of runtime operations by operation type.
# TYPE kubelet_runtime_operations counter
kubelet_runtime_operations{operation_type="container_status"} 472
kubelet_runtime_operations{operation_type="create_container"} 93
kubelet_runtime_operations{operation_type="exec"} 1
kubelet_runtime_operations{operation_type="exec_sync"} 533
kubelet_runtime_operations{operation_type="image_status"} 579
kubelet_runtime_operations{operation_type="list_containers"} 10249
kubelet_runtime_operations{operation_type="list_images"} 782
kubelet_runtime_operations{operation_type="list_podsandbox"} 10154
kubelet_runtime_operations{operation_type="podsandbox_status"} 315
kubelet_runtime_operations{operation_type="pull_image"} 57
kubelet_runtime_operations{operation_type="remove_container"} 49
kubelet_runtime_operations{operation_type="run_podsandbox"} 28
kubelet_runtime_operations{operation_type="start_container"} 93
kubelet_runtime_operations{operation_type="status"} 1116
kubelet_runtime_operations{operation_type="stop_container"} 9
kubelet_runtime_operations{operation_type="stop_podsandbox"} 33
kubelet_runtime_operations{operation_type="version"} 564

# HELP kubelet_runtime_operations_latency_microseconds Latency in microseconds of runtime operations. Broken down by operation type.
# TYPE kubelet_runtime_operations_latency_microseconds summary
kubelet_runtime_operations_latency_microseconds{operation_type="container_status",quantile="0.5"} 12117
kubelet_runtime_operations_latency_microseconds{operation_type="container_status",quantile="0.9"} 26607
kubelet_runtime_operations_latency_microseconds{operation_type="container_status",quantile="0.99"} 27598
kubelet_runtime_operations_latency_microseconds_count{operation_type="container_status"} 486
kubelet_runtime_operations_latency_microseconds{operation_type="list_containers",quantile="0.5"} 29972
kubelet_runtime_operations_latency_microseconds{operation_type="list_containers",quantile="0.9"} 47907
kubelet_runtime_operations_latency_microseconds{operation_type="list_containers",quantile="0.99"} 80982
kubelet_runtime_operations_latency_microseconds_count{operation_type="list_containers"} 10812
kubelet_runtime_operations_latency_microseconds{operation_type="list_podsandbox",quantile="0.5"} 18053
kubelet_runtime_operations_latency_microseconds{operation_type="list_podsandbox",quantile="0.9"} 28116
kubelet_runtime_operations_latency_microseconds{operation_type="list_podsandbox",quantile="0.99"} 68748
kubelet_runtime_operations_latency_microseconds_count{operation_type="list_podsandbox"} 10712
kubelet_runtime_operations_latency_microseconds{operation_type="podsandbox_status",quantile="0.5"} 4918
kubelet_runtime_operations_latency_microseconds{operation_type="podsandbox_status",quantile="0.9"} 15671
kubelet_runtime_operations_latency_microseconds{operation_type="podsandbox_status",quantile="0.99"} 18398
kubelet_runtime_operations_latency_microseconds_count{operation_type="podsandbox_status"} 323
复制代码

可以通过 Prometheus 对其进行监控:

5. 总结

以我的经验,造成 PLEG is not healthy的因素有很多,而且我相信还有更多潜在的因素我们还没有遇到过。我只提供几个我能想到的原因:

  • RPC 调用过程中容器运行时响应超时(有可能是性能下降,死锁或者出现了 bug)。
  • 节点上的 Pod 数量太多,导致 relist无法在 3 分钟内完成。事件数量和延时与 Pod 数量成正比,与节点资源无关。
  • relist 出现了死锁,该 bug 已在 Kubernetes 1.14 中修复。
  • 获取 Pod 的网络堆栈信息时 CNI 出现了 bug。

6. 参考资料

微信公众号

扫一扫下面的二维码关注微信公众号,在公众号中回复◉加群◉即可加入我们的云原生交流群,和孙宏亮、张馆长、阳明等大佬一起探讨云原生技术

技术面试中,什么样的问题才是好问题?

$
0
0

其实很久以前就想谈一谈这个话题了,但是最近才有了足够的动机。因为从最近参加的很多 debrief 来看,我认为身边大多数的软件工程师面试中,在通过技术问题来考察候选人这方面,很多都做得不够好。比方说,我看到对于一些经验丰富的软件工程师候选人的面试,一些面试官依然是草率地扔出一道算法题让做了事,并且认为能不能够比较清晰完整地将代码写出来,是工程师级别裁定的最重要的标准。而这样的做法我认为是非常不妥的。

首先,我要明确的是,这个问题,指的是技术面试中俗称的 “主要问题”,具体来说,就是面试官会拿出一个问题和候选人讨论,并通过由此开始双方的互相沟通和问题发散来达到考察的目的,因此,这个 “问题”,从某种角度说,更像一个 “话题”。这个过程通常在每轮面试中会持续几十分钟(如果你对这种面试方式感兴趣,你可以看一下这个 简单的介绍)。下面的讨论,都是建立在这种面试风格和方式之上的。

其次,作为一个 disclaimer,我想说,以下内容来自于我的认识,并且是针对于技术面试这一个狭窄范围内的认识,自然带有主观的倾向性和认知的局限性,它并不是来自任何公司或组织的标准。

好,下面我就来尝试把这个问题讲清楚、讲透彻。我认为这并不是一件容易的事情,因此如果你对其有不同的看法,欢迎和我一起讨论。

典型案例

我先来举这样一个典型的例子,这里面包含了若干个值得商榷的方面,你可以看看是不是似曾相识:

在和候选人谈论完项目和经历以后,面试还有 40 分钟,于是面试官问:你能否实现一个 LRU 队列?

于是候选人想了一下,就开始做题了,也就是在白板上大写特写,于是面试官也就开始忙自己的事儿了。等到 40 分钟后,候选人写了一白板,但是显然,他在这过程中遇到了一些困难,最后虽然实现了,但是代码写得有些复杂,也遗漏了两、三个重要的 corner case。

于是面试之后,面试官在评语中写道,“候选人能力一般,算法题实现起来磕磕绊绊,最后的代码偏臃肿,而且有明显的 bug”。

在往下阅读以前,请你想一想,这样的面试形式有哪些值得商榷的地方?

技术面试的目的

好,我先卖个关子,先不回答上面的问题,而是先谈一谈,对于软件工程师候选人来说,我们为什么要进行这样的技术面试。事实上,有很多考察项,完全不需要技术面试这样麻烦的途径,就可以很容易、很高效地实现;而下面我说的这些方面,这些对于软件工程师来说至关重要的方面,技术面试却是合理考察的唯一可行途径。

技术能力方面

通常不同轮次的面试,会考察不同的技术点,这个对于不同的团队和职位,是不一样的。

举例来说,对于某业务平台团队的一个高级工程师的职位,五轮面试中,一轮考察项目和经验,一轮考察系统设计,两轮考察具体问题的解决,特别包括算法和数据结构,还有一轮考察面向对象设计等其它内容,这其中,后三轮都包括白板编码的考察。

对于一个有经验的工程师候选人来说,我认为这就是一个比较立体、综合,也是一种比较合理的技术考察点的划分方式。并且,这五轮中有四轮都会花费大量的时间,通过我今天将谈到的技术问题,来对候选人进行技术能力方面的评估。这里,有这样几个非常常见的技术方面的考察项:

  1. 分析问题,整理需求的能力。问题在一开始可能很模糊,但是优秀的且有经验的工程师可以识别出核心的诉求来,这个 “识别” 的能力,下文我还会详述。这里的诉求可能有多个,但是考虑到时间的关系,面试过程中往往只会从某个角度覆盖其中一两个。
  2. 根据需求来设计系统的能力。这里既包括功能性需求,又包括非功能性需求,前者是必须要涉及到的,但是后者也经常也放在一起考量。其实这一点可大可小,它未必一定指系统设计中,功能是否得到实现,并且这个过程中,可能会涉及到系统的扩展性、可用性、一致性等等方面。
  3. 将核心逻辑实现的能力。如今,考察比重容易被高估的算法和数据结构就大体上属于这一部分。它也有功能和非功能的两个角度——功能上算法能否实现需求,非功能上算法是否具备足够的性能,编码是否遵循最佳实践,代码是否具备良好的可扩展性等等。而编码能力,指的是在思路达成一致以后,核心逻辑能不能落到纸面(代码)上。毕竟,“空谈误国,实战兴邦”。
  4. 经验和其它工程能力。这部分相对更为灵活。比如对测试能力的考察,即可以做怎样的测试来实现对于功能需求和非功能需求正确性的保证。对于特定的团队和项目来说,有时候会特别专注于特定的技术能力,比如前端的团队,是需要考察前端的基础技能的。

考虑到时间和覆盖面、覆盖深度的权衡,上面这四点有时候不能在一轮面试中完全覆盖,往往也会包括三点。比如,系统设计的面试可以着重覆盖 1、2、4 点,而编码为主的面试可以着重覆盖 1、3、4 点。

非技术能力方面

和技术能力考察所不同的是,非技术能力考察在不同面试官中的特异性更大,换言之,每个人的角度和标准都可能不相同。但我觉得下面这几条是特别重要,因而必须要覆盖到的:

  1. 沟通合作的能力。如果不计时间成本,最理想的面试是什么?其实就是一起工作。工作中才会有足够多必要的沟通,无论是正面的品质还是负面的问题都会无所遁形。可是面试的时间有限,我们没有办法实现真正的工作氛围,但依然可以模拟工作中一起考察、分析和解决问题的过程,而这个过程,就是要通过 “沟通” 串起来的。沟通是一个大的话题,具体的考察项有很多,比如,能不能接受建议?能不能讨论想法?有些候选人一旦进入自己的思考模式就听不进别的话了,于是一个点要反复强调几遍;而有的则是缺少 backbone,稍微追问一下,也不思考,就立马改变主意。
  2. 热情和兴趣。热情和兴趣的影响是巨大的,都说兴趣是最好的老师,这部分是很难 “教出来” 的。对于初级工程师来说更是如此。热情和兴趣不但会影响到他/她自己的未来发展,也会影响到整个团队的氛围。
  3. 学习能力。学习能力很大程度决定了候选人在新的岗位上进步的潜力。同样的基础和基本的问题解决能力,有的候选人能够 “一点就透”,触类旁通,这在一定程度上就反映了学习的能力。毫无疑问,软件工程师每天都在面对新问题,入职以后就会发现,不止问题是新的,代码是新的,类库是新的,工具是新的,在成熟到能够有一定产出之前,这一步一定是学习。

技术能力和非技术能力,哪个更占主导?事实上,这二者都很重要,并且二者各自又具备不同的特点。当然相对来说,非技术能力更加难以提高,因而这方面的问题要更加引起重视。比方说,要让一个对于软件领域缺乏热情和兴趣的候选人,在入职以后改头换面,是几乎不可能的一件事情。

其它方面

上面说的是技术面试中对于 “能力” 的考察。其实,还有一些考察项,严格来说并不能算是 “能力”,因此我就没有归类在上面,也不是本文的重点,但这并不是说它们不重要。

比如,候选人是否具备正直的品格。在 OCI,debrief 的结果,通常有 hire 和 no hire 两种,但是有一种情况,可以归结到一个特殊的 “never hire” 里面去,这样的候选人没有面试冷冻期,不会有职位和级别的讨论,就是一个 “永不考虑” 聘用——这就是品格问题。品格问题会导致 never hire 的出现,比如候选人对当前的所在职位说谎了。

再比如,和团队的契合程度。不同的团队,接纳候选人的程度和要求都是不一样的。一个典型的例子是,有时候我们发现,有的候选人在投票的边界线上,即本身是具备相当的潜力的,但是由于相对缺乏领域经验,且某些方面显示出方法明显不得要领。如果团队中有成熟、有经验的工程师可以带着,且团队有一定的空间允许他花更长的时间学习和成长,那么最后的结论就是 hire,否则就是 no hire。你也可以看出,很多时候这样的决定都不是非黑即白的,影响的因素是多方面的。

再再比如,性格不兼容导致的风险。我遇到过一例,候选人在面试过程中,在多轮面试中都表现出高傲和自满的个性来。于是这成为了一个担心招聘进来以后,风险过高的重要方面。于是最后我们放弃了这个候选人,尽管这个候选人的技术方面是没有问题的。有人可能会说,聪明人都是有个性的。但其实 “有个性” 和 “难相处” 却有着微妙的差别,而且再包容的团队,也有自己的底线。我们当然不希望错过优秀的人才,但是这并不是不计代价的。

从这几个方面也能看出,这些 “非能力” 的考察项,往往具备着或 0 或 1 的 “red flag” 的特点。面试官一般不会花心思在这部分的考察上,但如果发现这方面的问题,且经过了明确。那结果往往就会是一个明确的否决票,而这个否决票是和级别、职位无关的。

回看那个案例

讲完了技术面试的目的,再来回看那个案例。那个案例中,所记叙的面试过程,对于技术面试的技术能力和非技术能力的考察,是否有覆盖呢?我们不妨一条一条看吧:

技术能力方面:

  1. 分析问题,整理需求的能力。这一条考察的程度明显是不够的,给出的问题,是一个明确的、具体的算法题,也就是要实现一个 LRU 队列。也许这其中存在着问题分析和需求整理的空间,但对于具体算法题来说,这个空间显然并不大,而且候选人闷头就写了,这方面无从考察。对于一个初级工程师的面试来说可能还好一些,我通常没有微词;可对于一个要去面试一个经验丰富的工程师,我是很不赞成纯算法题面试的,而这就是其中的一个重要原因。
  2. 根据需求来设计系统的能力。这一点的覆盖基本为 0。上来就写代码了,不清楚思考的过程,也更谈不上什么系统设计了。
  3. 将核心逻辑实现的能力。这条确实是这种面试方式能够覆盖的部分,因为整个过程,就是候选人思考并编码实现的过程,只不过,面试官能得到的只有一个 “结果”,而非整个 “过程”。这样的数据,能反映出来在核心逻辑实现方面的价值,就要大打折扣了。
  4. 经验和其它工程能力。也许能够从代码的实现上获知一部分,但这一条考察的程度也显然是很不够的。

再来看非技术能力方面:

  1. 沟通合作的能力。这是最大的问题,因为这方面是远远不足的,整个过程没有沟通,没有合作,只有默默地做题。
  2. 热情和兴趣。这个过程很难从这个角度获取足够的候选人在热情和兴趣方面展示出来的信息。
  3. 学习能力。同上。

也就是说,除了 “将核心逻辑实现的能力” 可能还勉强过得去,这样的面试方式,并无法全面、合理地考察候选人作为软件工程师的综合素质。

事实上,如果你联想实际工作。如果你的团队中有这样一个工程师,拿到一句简单的需求,不确认问题,不沟通设计,不讨论方案,直接就开始埋头苦干,就算能写出可以工作的代码来,这是不是依然是一件无比恐怖的事情?显然,我们的面试要尽可能避免这样的事情在真实世界中发生。我们要找的是软件工程师,不是只会刷题编码者。

这也是我把这个案例,放在开头,作为反面案例的原因之一。

等等,“之一”!难道还有别的原因?

是的,这还没完,这个案例还有着其它弊端,我想再卖个关子——而现在,你可以想一想,再往下看。

怎样的问题才是 “好” 问题?

终于要正面回答标题中的问题了,到底怎样的问题才能真正称得上 “好” 问题呢?下面是我认为最重要的几条衡量标准。

从模糊到清晰

首先,这个问题在一开始要足够模糊,以便让候选人可以逐层递进,逐步细化,寻根究底。 这个过程,其实就是将 “具体问题” 经过分析、归纳、思考、抽象并将其映射成为一个 “软件问题” 的过程。在问题变得清晰的过程中,理想的情况是,候选人可以表现出主动性,即候选人可以在多数情况下引领讨论的思路,而不是面试官。面试官需要顺着候选人的思路,逐步框定下问题的讨论范畴,并明确到其核心实现是确实可以用软件的办法实现的。

在这样的状态下,候选人可以以自然的状态,具备相当自由度地发挥自己的能力。从这个过程中,可以观察得到太多候选人的不同角度的特质了。通过这种方式,也可以很大程度避免了已经知道 “标准答案” 的面试官,由于思路的局限性,而给面试施加的源自于主观偏好的影响。

这就好像是开放世界的 RPG 游戏,有多个不同的路径都可以完成任务,玩家可以决策并决定主角的走向,但是这一切始终还要在游戏设计者的掌控之内。这当然是说的理想状态,有时候会有偏差,但我们朝着这个方向努力。这也对面试官驾驭不同的状况有着很高的要求,毕竟,面试官要对这个问题前前后后足够的熟悉,以便应对各种不同的细化场景。有一个常见的方式,是可以从一个自己已经足够熟悉的问题开始,比如自己曾经多年工作涉及的某类系统。

我来举一个具体例子。比如,有这样一个问题:

怎样设计一个流量控制系统?

这就是一个模糊到没法再模糊的问题了。不知你会不会产生下面这样的问题:

  • 什么系统需要流量控制?
  • 现在的流量是多少?
  • 需要支持到什么时间精度?
  • 流量控制的规则怎么定义?
  • 超过流量的请求怎么处理?
  • ……

其实,这些都或多或少是需要面试官和候选人一起逐步思考、分析和明确的。在这个过程中,可以考察的内容太多太多了。

事实上,针对不同程度的候选人,上述这个问题给出的最原始的模糊程度是不一样的,问题越是模糊,这部分对于候选人的要求也就越高。对于一个工作十多年的,有着多年系统设计经验的工程师来说,上面这些问题大致都应该是他/她能够主动提问,或是主动引领明确的。

值得一提的是,理想的问题最好还有一些隐藏的 “坑”,能否把这些坑识别出来,也是对于工程能力方面,一个很好的小的考察点。比方说,优秀的候选人应该想到,流量控制可以基于绝对时间窗口,或是相对时间窗口来进行的,但是要真正保护系统,相对时间窗口才是最理想的。当然在实现难度上,相对时间窗口,往往会更难一些。

而对于一个没有工作经验,并将要研究生毕业的候选人来说,问这样一个模糊的问题,往往带来了过大的难度,不但不容易推进面试的进程,还可能给候选人带来沮丧的心理。我们不希望看到,候选人拿到问题以后就懵了,如果发现候选人推进有困难,面试官需要介入并帮助。

因此根据候选人的程度,这需要面试官主动回答这些问题,或是直接缩小或明确问题的范畴,当这个问题的范畴缩到最小时,这可以是一个直接存在多种解法的算法题。极端地说,这个问题可以一直缩小到这样的程度:

假定说有这样一个 API,名字叫做 isAllowed,这个 API 在系统每次收到请求的时候就调用,传入的是请求对象,传出 boolean 值表示是否允许这次调用——如果最近一分钟内调用次数小于 10000 次就允许,反之则不允许。你能否将这个 API 实现出来?

如果候选人还一脸迷茫,可以提供这样的参考 API:

1
2
3
4
5
classRateLimiter {
    publicbooleanisAllowed(Request req) {
        // TODO
    }
}

你看,这只是一个将问题明确、细化和分解的过程,并没有涉及到实际实现代码该用的算法。但是,上面提的那些问题,要么都通过这个例子明确了,要么都给出具体数字了,这本身,就将一个模糊的问题,降低难度明确为一个具体的算法问题了。

不止一个解

前面一步已经谈到了有不同的方式可将模糊的 “实际问题” 映射到了一个可解的 “软件问题”,那么现在,这个 “软件问题” 依然没有标准答案。可能有几个参考答案,它们互相比较起来各有优劣。大多数情况下,候选人的思路,都在这几个参考答案的思路之中,但有时也能看到特立独行的新奇思路。

如同前一步所说的那样,对于不同级别的软件工程师职位来说,需求分析、系统设计等等这些方面的要求可能有着很大的差别;但是在这一步,对于数据结构和算法这样的基础能力,却是接近的。

对于这里谈到的流量控的算法来说,实现方式是有很多种的,代码复杂程度,控制精度,时间复杂度和空间复杂度等等都有着非常大的区别。当然此时涉及到的,已经基本只是算法层面的话题了,就算法本身而言,我在极客时间专栏中对其中的几个典型方法 做了一些介绍,感兴趣的话可以阅读。

我可以再举一个我曾经经常在面试中拿来使用的例子:

某社交网站有两百万的注册用户,每个用户都有积分属性,且积分根据用户在社交网站上的行为而不断有小幅度的频繁变更(比如登陆一次就+1 分,评论一次就+2 分等等),怎样设计一种算法,能够高效、准确地实时获取指定用户在所有用户中基于积分的排名?

上面的问题从模糊逐步落实到实现上的时候,异步、定时地排序,是最容易想到的方案,而题目表述中的 “高效” 和 “实时” 这两个修饰词让这个问题变得困难。这个过程中,我见到的不错的办法就至少有七、八种,比方说,下面这个推进问题解决的例子:

  1. 候选人:在需要的时候进行排序,方案是……
  2. 面试官:好,这样的方式下,时间、空间复杂度是多少?
  3. 候选人:……(说着说着自己意识到时间消耗可能巨大)
  4. 面试官:对,不仅时间、空间都消耗巨大,CPU 也是,你能否优化?
  5. 候选人:……(提出了一些优化思路,但是他自己对它们的实时性也不满意)
  6. 面试官:好,有换个角度更进一步优化的方式吗?
  7. 候选人:对了,可以让数据一直是排好序的!
  8. 面试官:好主意,那你怎么设计数据结构呢?
  9. 候选人:我可以使用一个 map 来保存用户 id 到积分的映射,再把积分从小到大按序放在数组中,这样二分查找就可以找到对应积分所处的排名。
  10. 面试官:听起来不错,那么这时候获取排名的复杂度是多少?
  11. 候选人:……
  12. 面试官:对,每当用户的积分小幅变化的时候,你怎么维持这个数组依然有序?
  13. 候选人:从数组中拿掉一个老的积分,再放入一个新的积分……
  14. 面试官:这个变更影响的数据量有多少,时间复杂度又是如何?
  15. 候选人:……
  16. 面试官:不错,可这个方法有什么问题吗?
  17. 候选人:(恍然大悟)如果新添加一个用户,新的积分会出现在数组头部,数组内的所有数据都要向后移动一个单位!
  18. 面试官:没错,那你打算怎么优化?
  19. 候选人:可以把数组内的积分从大到小排序,这样新添加的用户所对应的积分总在尾部。
  20. 面试官:很好,这个方法还有什么问题吗?
  21. 候选人:……(意识到在某些情况下,有很多用户拥有相同的积分,这时时间复杂度会退化)
  22. 面试官:那样的话,你怎么优化?
  23. 候选人:数组的元素除了记录当前积分,还记录有多少个用户具有这个积分,从而消除相同积分的重复元素……
  24. 面试官:很好,可这个方法会带来一个问题,你能想到吗?
  25. 候选人:对了,如果积分变化以后,新的积分是没有出现过的,那么添加到数组里,就是一个新元素,于是所有比它小的积分全部都要向后移动一个单位。
  26. 面试官:非常好,那么你怎么优化?
  27. 候选人:如果使用链表来代替数组就可以避免这个问题,(突然意识到)可是链表我就没法二分查找了……
  28. 面试官:没错,那什么样的数据结构和链表在这方面具有一定相似性,又能够具备二分查找相似的时间复杂度?
  29. 候选人:……(这一步能回答出来答案就很多了,很多都是很不错的思路,比如有用跳表的,有用二叉搜索树的等等)

这只是一个简化了的片段,实际的沟通的内容远比这部分内容多,但是从中也依然可以管中窥豹,看出问题解决的过程是怎样逐步推进的。

从这里也可以看出,无论是从实际问题细化到软件问题,还是求解这个软件问题,都存在着多条通往罗马的道路,看起来很美好,但这样的问题设计和面试把控并不容易。但既然大家都是软件工程师,是未来有可能一起工作的工程师,面试官的能力和可能就和候选人接近,于是,为了保证面试的效果,就一定要精心准备这样的问题,而不能指望随机和临场想出来一个 “好” 的问题提问。

围绕问题的解决要完整

这个问题的分析、讨论和解答过程要完整。对,其实这一点说的已经不是问题本身了,而是攻克这个问题的过程了。

这指的是 整个过程要努力让候选人能够抵达 “踮踮脚能够到” 的难度,并且能够完成从确认、分析、讨论、编码、验证和改进等一个过程。这让整个面试显得完整,同时带来了这样两大好处:

  • 对于面试官来讲,这样一个完整的过程,可以更全面地考察候选人,避免陷入视角过窄和一叶障目的情境。同时,“踮踮脚能够到” 的难度,又可以给整个考察的进程具备较为合理的预期。
  • 对于候选人来讲,心态可以得到一定程度的平复,不沮丧,能够 “完整地” 面试完一轮,能够收获信心。别忘了,面试是双向的,给候选人一个良好的印象是很重要的。

前文我举的这个将问题从模糊到逐步清晰化的这个例子,就是一个需要面试官根据候选人情况动态调整的例子。 在候选人能够经过思考而快速推进问题解决进展的时候,要让出主动权,以被动回答和鼓励为主;但在候选人卡壳的时候,要夺回主动权,及时给出提示和引导。

在落到数据结构和算法上面的时候,极少有候选人能够在叙述思路的时候直接给出最优解的。这时候,如果时间充裕,特别是在候选人进展非常顺利的时候,可以不断提示、追问以要求 “代码前优化”,一步一步优化到他/她的问题解决的能力边界,这就是其中的一个探寻其 “踮踮脚能够到” 的这个问题的解决能力的一个办法。但这个过程的前提,是一定要给编码留足时间。当然,如果候选人不能在限定时间内给出清晰的优化后思路,那不妨就退一步到原先那个算法角度不那么 “好”,但是思路清晰的解法上,并落实到代码。

比方说,对于前文所述的那个流量控制的问题,候选人在还有半个小时的时候就想到了使用一个时间复杂度为 O(N) 的解,而面试官认为时间还比较充裕,那就可以尝试挑战一下候选人 “能否再优化一下复杂度?”。

有些时候,由于前面的过程磕磕绊绊,时间剩下不多了,依然只有时间复杂度较高的 brute force 的解法,那么将这个解法实现了,其实也是一个不错的选择。有时候时间实在比较紧张,可以要求实现一部分核心代码,这些都比由于时间太短而代码写了一半匆匆收尾要好得多。我的经验是,在讨论充分,思路清晰的情况下,代码完成的时间,一般只需要 10 到 15 分钟,这样的代码量对于面试来说是比较合理的。

当然,相较而言,一种更为糟糕的结果是,一直到最后,讨论的深度依然离编码尚远,甚至依然停留在一个很高的泛泛而谈的层面。

如果在编码完成之后,尚有时间,优秀的候选人会拿实际的例子去验证代码的正确性。而面试官也可以和候选人讨论 “代码后优化”,比如以下的问题:

  • 你能否进一步优化算法以提高时间/空间复杂度?
  • 如果是工业级别的代码,你觉得代码还有哪些问题?
  • 你该怎样去设计测试,来保证这段代码的正确性 ?

对考察项的覆盖兼有深度和广度

这个问题要能够考察前文所述的技术能力和非技术能力。

这里说的覆盖,不一定要全部覆盖,但是要覆盖其中的大部分,并且对于每一个考察项要具备一定的考察深度。我见到过不少其它的面试风格,但我认为这样就着一个模糊的主要问题(话题)逐步展开的方式是最好的。因为它可以兼具广度和深度的平衡。具体来说:

面试很容易走向的一个极端就是考虑广度,但缺乏深度。比如一种风格是绝大部分的面试时间用来询问候选人的项目和经验,让候选人自己介绍,而面试官跟进追问。这原本是一种很好的方式,但是由于候选人对自己的项目通常远比面试官熟悉得多,除非明确的同一领域,否则面试官较难对于其中的内容挖掘到足够的深度,从而识别出候选人是真正做事的人,还是夸夸其谈的人。这也是这种方式理论可行,但实际开展难度较大的原因之一。

另一个极端,自然就是考虑深度,但缺乏广度。比如给出一个过于具体的问题,缺乏发挥和迂回的空间,对这个问题所涉及的很小一部分深度挖掘,甚至纠缠于某个特殊而单一的 case 很长时间,但是却只能覆盖很少的考察角度。

由于深度和广度都是可控的,那么这样的可以拿来问不同经验和不同技术背景的工程师候选人。这样对于面试官来说,可以获得足够的数据,便于在遇到新的候选人的时候,能够进行横向比较,做出更准确的评估。比方说,过去某级别的候选人能够在面对这个问题的时候,能够达到什么样的级别,而如今这个候选人有着类似表现,这就可以以过去的那个例子来作为参考比较了。

再次回看那个案例

现在,让我们再次回到文章开头那个例子问题,除了已经提到的考察项的覆盖不够,它还有哪些问题呢?参考上文已经提到了 “好” 问题的标准,我觉得其中的这样两条是违背的:

从模糊到清晰。显然,问题给出的时候就已经相对比较清晰了,这样的方式并不能模拟软件工程师日常面对的许多模糊而困难的实际问题。我已经提到,对于经验丰富的候选人来说,这样的问题无法重现将实际问题映射到软件问题这样的一个重要思考过程。

另一个是,围绕问题的解决要完整。示例问题的考察过程中,只有缺乏互动的编码环节,没有其它过程,没有编码前的分析、思考和优化,也没有编码后的测试、改进和优化。考虑到 LRU 完整算法是一个实现代码量偏大的问题,拿来放到面试中做一个完整实现,由于会消耗过多的时间,挤压其它时间,因此显得有些过了。

其它 “不好” 的问题

前面说了过于清晰的纯算法题的 “不好” 之处,也说了 “好” 问题的例子,最后我想再来说几类其它的且典型的 “不好” 的例子。

被使用过太多遍的问题

很多公司(如 Google 和 Facebook)都有参考题库,题库会不断更新,其中一个重要原因,就是要尽量避免候选人做过被问到的题目。而且,问题越明确和具体,一旦候选人做过,考察的效果就越不客观。比方说,下面这个问题本来是个很不错的问题,但由于是被过于广泛地使用了,因此我还是不建议拿它来用作面试题的:

怎样设计一个短网址系统?

依赖于特定语言或框架

而这里说的依赖不好,是因为考虑到候选人不同的技术背景,如果没有特殊的需要,避免这样的依赖,以避免产生不应有的错误的衡量数据。

比方说,问一个关于 JVM 的问题,这个问题可以问,特别是在候选人强调其具备一定 Java 背景的前提下。但是这样的问题不能成为 “主菜”,尤其是候选人不一定具备很强的 Java 背景,这样的方式会导致考察的偏颇。

过于复杂的规则或背景知识

这主要是基于面试的有限时间考虑的,过于复杂的规则或背景知识,容易把时间消耗到澄清它们上面;另外,有些背景知识并非是所有人都熟知的,这就会引起考察标准的非预期。我给一个经典的反面例子:

设计一个算法,把一个小于一百万的正整数,使用罗马数字来表示。

这个问题描述简洁,但是拿来做技术面试中的主要问题,其中一个不妥之处就是,不是所有人都很清楚一百万内的罗马数字表示法的。对于不清楚的人来说,要搞清楚这个表示法的规则,就已经是一件有些复杂和耗时的事情了。显然,这个罗马数字表示法的知识点,不应该成为考察和衡量的标准。

当然,有时候有些问题的背景知识是冷门的,但是表述简洁,那么这样的话只要 面试官能够主动、迅速地说清楚,拿来使用是没问题的。

知识性问题

我想特别谈一谈,对于知识性问题的提问。在讨论问题的过程中,如果涉及到关联的具体知识,那么提问一些基础知识是一个不错的选择,这是考察候选人 CS 基础是否牢靠的方式。比如候选人提到使用 HashMap,而他/她最熟悉的编程语言是 C++,那么询问在 C++中 HashMap 是怎样实现的就是一个可行的问题。而且,由于这样的问题是嵌套在前述的这个大流量控制问题的解决过程中的,更显得自然而不突兀。

反之,如果这样的知识性问题较为冷门或浅表,和当前的讨论中的问题无关,或是不在候选人的知识体系内,这样的问题就值得商榷了。比如,仅仅因为自己的项目组使用 Spring,面试官就询问候选人 Spring 的 bean 的单例和多例分别是怎样配置的,而恰巧候选人又是 C++背景,对于这部分并不熟悉,这样的问题除了给候选人造成困扰以外,意义就不太大了。

如果把知识性问题作为主要问题来提问,我认为是不适合的。因为一方面它不具备普适性,另一方面它又具备太强的随机性。

这里不具备普适性这一条,指的是不同技术背景的软件工程师候选人都要能够适合参与这个问题的解答。举例来说,如果问 “Tomcat 的线程池的配置策略?”,这就是一个不具备普适性的问题,这是一个偏重 “知识性” 的问题,如果候选人没有使用过 Tomcat,或是只是略有了解,很可能就栽了。

而具备太强的随机性这一条,指的是一旦待考察的问题具体以后,这个问题就容易从对 “能力” 的考察变成了对 “知识” 的考察,而这个知识,又恰恰是比较容易随机的。也就是说,我们不希望候选人 “恰巧” 知道或不知道某一个细小的知识点,来决定他/她是否通过这项考察。

无论如何,知识性的问题作为考察的辅助方式可以,但不应成为主角。我还是觉得我们应当能把更多的时间,留给主要问题的解决过程本身。

关于 “技术面试中,什么样的问题才是好问题?”,我就说这么多吧。也欢迎你分享你的看法,我们一起讨论。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》

大规模微服务场景下灰度发布与流量染色实践

$
0
0

最近微服务很热,与微服务相关的架构、流程、DevOps都很热。很多公司,包括传统企业,到互联网公司做交流的时候,会问道,你们互联网公司号称能够加速业务创新、快速迭代,那我们是否也可以引入类似这样的机制。

我们做微服务,主要分为两个方面,一个是业务方面,另一个是技术方面。最下面是运维部,不过现在我们的运维部已经拓展成云计算,DBA里的数据管理部门,已经发展成大数据,于是就有了技术中台和数据中台,另外还有共享用户中心的业务中台,总体构成了下层的中台部门,在上层业务一定要做微服务化。业务和技术互相合作,做到加速创新的效果。
1.jpg

有很多人说,我们也上了微服务,但是会发现上微服务以后,看起来很好的东西,为什么用起来一团乱麻。

我们拜访过很多业界同仁,发现实施微服务之后,有以下痛点:
  • 服务依赖管理:服务间直接调用,依赖混乱(微服务越来越多,自己理不清楚,不知道上线时会影响谁,上线后谁影响我,到底该什么时候上线,依赖混乱的时候,没办法解决这些问题。)
  • 服务调用统计:调用记录无迹可寻,调用统计与分析无从谈起
  • 服务接口规范:环境与接口规范缺失,维护困难
  • 服务安全管理:安全靠白名单各自为战
  • 服务治理能力:大量重复代码 实现路由,分流,熔断,降级
  • 服务接口测试:拆分过程中接口行为不一致,隐藏Bug
  • 服务灰度发布:上线功能实现灰度借助大量if-else
  • 服务压力测试:对于峰值压力无历史数据,靠运气
  • 服务调用链分析:当服务请求缓慢,难以定位问题点
  • 测试环境治理:测试环境多,难管理,不可能100个容器每组一套


2.jpg

我们发现大家对微服务有很多误解。比如,一般做微服务的时候,很多人都会问微服务怎么拆,告诉我一个拆的最佳的实践,但是其实,根据我们的实践来讲,微服务不仅仅是微服务拆分,微服务拆分只是十二个要点的其中之一。

十二个要点分别是:
  • 微服务化的基石:持续集成
  • 静态资源分离与接入层设计
  • 应用层设计之无状态化与容器化
  • 应用层设计之服务的拆分,发现与编排
  • 性能优化之数据库设计与横向扩展
  • 性能优化之缓存的设计与横向扩展
  • 性能优化之消息队列与异步化设计
  • 服务的熔断,降级,限流设计
  • 配置中心的设计与实践
  • 统一日志中心的设计与实践
  • 全链路应用监控实践
  • 服务的全链路压测实践


我们建议,先把前三个基础打好,再进行拆分,而不是什么技术、平台、工具都没有,直接把自己的传统应用拆得七零八落。同时,值得再强调的是第一条,微服务化的基石:持续集成。微服务绝不是让大家关起门来用三个月的时间拆出来,就直接上线。而是应该不断地集成、迭代,是渐进式的模式。另外,微服务也不仅仅是个技术问题,它还涉及到IT架构、应用架构、组织架构的改变。
3.jpg

接下来给大家讲一下网易微服务和DevOps的实践过程。

我们整个DevOps,也是经历了几个过程。第一个和大家都一样,当服务比较少的时候,开始手工化的方式,后来手工不行了就变成了脚本化的方式,再后来因为开源有很多的工具可以用,变成了工具,而后变成一个平台,最后变成一个统一的DevOps的平台。
4.jpg

首先,第一个阶段就是手工化。可能很多企业一开始都会存在这样的阶段,开发和运维之间的隔阂比较严重,老死不相往来。开发负责写代码,线上的运维、发布,以及SLA的保障,都是运维进行管理的。由于服务相对比较少,用物理机部署,基本上是一个单机应用加一个Oracle就可以搞定。
5.jpg

后来,随着业务的发展,服务越来越多。这个模式和原来还是没有变,开发和运维部的隔阂依旧存在。但是,运维发现接的需求越来越多,需要部署越来越多,需要一个环境隔离的方式,因此一般会上一个虚拟化系统,业内主流是用VMware。这时候的部署方式一般是,Oracle部署在物理机上,其他业务系统都是部署在VMware上。部署东西多了,运维开始使用批量脚本试图解放人力,这属于第二个阶段-脚本化的阶段。虚拟化带来很多的优点,比如,粒度灵活,隔离性得到一定保证,不会在一台服务器上部署很多东西。

但是这个阶段也有非常多的问题。比如说发布脚本、逻辑相对复杂,时间长了以后,逻辑是难以掌握的。而且,如果你想把一个脚本交给另外一个人,也很难交代清楚。

另外,并且脚本多样,不成体系,难以维护。线上系统会有Bug,其实发布脚本也会有Bug。

虚拟机大量地依赖于人工的调度,需要运维人员非常清楚,要部署在什么地方。另外VMware还有一个问题,它使用共享存储,会限制整个集群的规模,因为此时的应用不多,这个程度的规模还可以接受。

线上的高可用性,业务层的开发人员不会做任何事情,他认为是线上一旦出事,应该由运维集中处理,迫使运维服务的发布人员依赖虚拟化机制,来提供高可用机制。我们都知道VMware有非常著名的简化运维的高可用机制,比如FT、HA、DR等类似的机制。如果我们是从IT层来做高可用,有一个缺点,作为基础设施层来讲,它看上层没有任何的区别,所以没有办法区分业务优先级。比如说FT的模式,跑CPU指令,它不知道这是最核心支付的指令、还是日志的指令,再如数据中心之间的同步,存储层是无法区分交易数据和日志数据的。

另外网络、虚拟化、存储等基础设施,没有抽象化的概念,复杂度非常高,开发接不了这个工作,必须依赖运维,就要审批。由统一的一帮人来做,而且他们要考证书,比如,网络要有思科的证书,虚拟化要有VMware的证书,要特别专业才能做这件事情,因此会极大地降低迭代速度。业务方无论做什么事,都要走审批,运维部的人根本忙不过来,这是第二阶段的问题。
6.jpg

后来是怎么改变了这个问题?首先是业务层,业务层接的需求越来越复杂,迭代速度要求越来越快,这个时候单体应用跟不上了,需要进入服务化的架构,工程要拆分,要开始基本的注册发现,要实现自己的RPC。
7.jpg

应用层的改进会带来应用层的问题。比如,服务雪崩的问题。大量的请求堆积,一个进程慢了,把整个链路也都变慢了,所有人都在等着它缓过来。我们要进行熔断,快速尝试另外的服务。原来依赖很多内网负载均衡以及硬件负载均衡的维护代价比较大,一旦出现任何问题,就会引来抖动的问题。所以相应的要有快速恢复、快速熔断的机制,一旦发现错误以后,我们要能够尽快的重试。
8.jpg

以上就是应用层的问题,经过了一段时间的解决,又引入了新的问题。我把它称为“云原生怪圈”,应用向云原生的(Cloud Native)。它包含两个层次,第一个层次是应用层的服务数目会增多。第二个层次是资源层申请速度的灵活性会相对增加,这两个层次形成了一个圈。每家公司可能都存在这个圈,无论是从哪个起点开始,这个圈都可能会被激活。

一个起点是,很多公司的上面是单体应用,但下面先采购了容器,资源申请灵活性大幅度提高了。一旦灵活性提高了以后,会给应用层释放很多动力。原来申请一百个机器需要一个星期的审批流程,这时能不拆分就不拆分。而现在有了容器,他会认为我有了这么好的工具,我可以进行拆分了,反正不费劲,任何一个小部门创建一个小的环境都不费劲。

另外一个起点,先是应用层服务数目增多,给资源层越来越大的压力,然后会使得你原来七八点下班,现在变成十点多下班,然后十二点下班,压力越来越大,就会想办法增加资源层的灵活性。这个圈在整个DevOps的过程中会一直产生的。

微服务化了以后,我们会发现存在以下几个现象:
  • 第一个是服务器的机型非常的碎片化,一开始采购机器的时候,有大规格、小规格的,硬盘比例各不一致,导致服务器非常难以管理,也无法进行批量化的安装。
  • 第二是很多的进程,不管是虚拟化以后,还是不虚拟化,在不在一台机器上,QoS无法保证。
  • 第三是测试环境的需求量大大增加,下层的基础设施根本忙不过来。


9.jpg

接下来进入到云计算的平台。有很多人不理解云计算和虚拟化都是运用了虚拟化的技术,两者之间到底有什么不同。其实云计算带来了非常大的不同,甚至是本质上的不同。如果你们内部上了一个云平台,或者上了公有云,但是你没有感受到资源申请的灵活性,那肯定是有些姿势用得不对。

这里,我总结了一下云计算带来的改变,主要有三大方面,分别是统一接口、抽象概念,租户自助。正是因为这三大方面,使开发和运维不像原来那样,有那么深的隔阂,而是开始逐渐互相靠近,开发部或者业务部开始进行一定的自助。
  • OpenStack实现接口统一,大部分部署工具支持其接口,可基于开源工具实现发布的工具化和平台化
  • Flavor抽象资源配比(4G 8G 计算优化型,网络优化型,存储优化型),统一硬件配置,提升利用率,硬件上线效率提升
  • 自动调度代替人工调度,区域可用区抽象对机房机架交换机的感知
  • 云提供租户概念,有账号子账号体系,有quota,可以让租户在管理员许可的范围内自助操作,加快环境部署速度
  • VPC屏蔽物理网络复杂性,冲突问题和安全问题,使得租户可自行配置网络
  • 基于虚拟机分层镜像发布和回滚机制,构建发布平台,可实现大规模批量部署和弹性伸缩
  • 基于虚拟机的PaaS托管中间件,简化租户创建,运维,调优中间件的难度
  • 发布平台提供基于虚拟机镜像+PaaS中间件的统一编排
  • 要求业务对于高可用性设计要在应用层完成


10.jpg

在这个阶段,要实现微服务框架与开源技术栈的统一。一开始微服务做的比较混乱,有用Spring Cloud,有用Dubbo的,需要一个统一的开源技术栈。另外,还要构建一个持续集成的平台,通过Agent和虚拟镜像部署软件包。
11.jpg

统一微服务框架之前,我们情况是这样的,一开始用服务注册服务发现,还是比较简单的。后来发现,我们还需要分流、需要降级、配置中心、认证鉴权、监控统计等,在业务代码之外加的越来越多,大家的代码写得到处都是,而且依赖于不同人的水平不一样,有的人写得好,有的人写得差,这就是一个当时遇到的问题。
12.jpg

后来我们就把它抽象成为了一个Agent,这个Agent在程序启动的过程中,通过jar直接带起来,使得统一的服务框架组件在Agent里面实现,并且提供统一的界面进行配置,这样业务方可以只写业务代码,基本上就搞定了这件事。
13.jpg

这样就形成了一个统一的微服务治理平台,并且后期会和Service Mesh做一定的融合。
14.jpg

因此解决了这些问题:
  • 应用减负:使用Agent和Sidecar技术,对应用无成本增强。
  • 开发减负:以微服务治理框架为设计目标、大幅减少重复框架代码、避免重复造轮子。
  • 版本控制:统一组件版本配置,避免隐性问题。
  • 兼容性:兼容的HTTP、RPC调用,兼容非Java应用。
  • 服务治理:根据业务线场景选择治理支持方法级别治理粒度。
  • 高性能:更低的性能损耗,并提供更细粒度的服务治理。


15.jpg

这时就有了发布平台。我们会把包放统一的对象存储上,通过Agent以镜像的方式进行下发。
16.jpg

这是我们的成果,内部都在用这款基于虚拟镜像的发布平台。
17.jpg

接下来又引入了新的问题,比如难以发现故障点、要引入故障注入服务,API版本混乱,这时需要引入API网关。
18.jpg

基于虚拟镜像的发布也很混乱,因为部署的应用越来越多,我们发现虚拟镜像的模板越来越多,会出现上千个无法复用的模板,好像每个小组织都有自己的一个东西,毕竟它还不是一个标准,所以接下来我们就拥抱了容器的标准。并且Auto Scaling原来是基于虚拟镜像的,现在使用Kubernetes来做,同时实现了分布式事务。
19.jpg

到了这个阶段,中间加了Kubernetes这一层。这里的更新包括,OpenStack可以做物理机的下发,Kubernetes作为统一的对接资源的编排平台,无论是VMware上,还是KVM机器上,还是物理机上,公有云上,上面都可以有Kubernetes统一平台。这个时候只需要对Kubernetes下发一个编排,就可以实现跨多个地方进行部署。

在基于虚拟机的运行环境和PaaS中间件之外,基于Kubernetes也可以有自己的容器镜像和运行环境,以及基于容器镜像PaaS中间件。发布平台原来是对接API的,现在有了Kubernetes以后,它可以非常平滑的通过统一的平台切换到Kubernetes上,所以,做一个发布平台,后面的对接还是比较标准的。

应用层也会越来越多,比如说有基于容器镜像的弹性伸缩,服务网格,分布式事务,故障注入等。
20.jpg

有了Kubernetes以后,就进入了Dev和Ops的融合阶段。这时我们发现,当服务数目再增多的时候,运维的压力也更大,如果所有的东西都要运维来做,其实是实现不了的。因此,我们建议环境交付提前,比如说一个容器镜像里面的子环境让开发自己去把控。他知道自己改了哪些内容、哪些配置,不需要通过文档的方式交给运维来做。容器镜像还可以做一个很好的事,它是非常好的中介,是一个标准,不论在那儿都可以,所以就产生了左边的太极图。运维会帮开发部做一些事情,开发帮运维做一些事情,这个时候进入了开发和运维融合的机制。
21.jpg

因为容器有非常好的分层的机制,如果开发不想写,可以让开发写大部分的基础环境。
22.jpg

另外一个建议叫不可改变的基础设施。当规模大了以后,任何一个节点出现了问题,都很难排查,所以我们建议对任何环境的修改,都要在代码的级别上修改。在部署平台之前,代码是代码,配置是代码,单实例运行环境Dockerfile是代码,多实例的运行环境编排文件也是代码。
23.jpg

持续交付流水线,是以Master和线上对应的,自己分支开发的模式。按需自动化构建及部署,线上环境还是需要人工触发的,但基本上是通过流水线代码处理的方式来做的。
24.jpg

容器化带来的另外一个问题,就是“云原生怪圈”再次起作用。服务规模越来越大,增加速度越来越快,需求指数性增加,大家都需要一个环境。比如一个集群一千个容器,如果三个小组各开发一个项目,想并行开发,每个人都需要一个环境,一下子需要三千个容器。这时候就需要中间件的灰度发布和流量染色的能力。
25.jpg

26.jpg

在最外层的网关上,可以做两个环境之间流量的分发,以及在微服务的Agent里面也可以做一个分发。最终,我们会有一个基准环境,就是Master对应的环境。
27.jpg

两个小组,一组开发了五个服务,另外一组开发了六个服务,他们部署的时候不需要一千个全部布一遍,只需要布五个,布六个。在请求调用的时候,从这五个里面互相调,不在这五个里面,在基准环境调,另外六个也是。这样就把三千个变成一千零十几个,环境大幅度减少。
28.jpg

这个时候环境的合并怎么办?环境合并和代码合并逻辑一致,统一在发布平台管理,谁后合并谁负责Merge。这是我们的一个效果,我们节省了非常多的机器。
29.jpg

有了流量染色功能,就可以做线上的灰度发布。这里我们会有几个环境,一个是预发类的环境,一个是小流量环境,还有一个主流的环境,测试的时候是可以进行染色。
30.jpg

我们以一天的整个开发周期举例子,每天早上初始化预发环境和小流量环境>>开启引流,进入持续发布周期>>代码发布到预发环境进行回归,预发环境为单节点部署>>预发通过后发布到小流量环境,小流量环境三节点部署,滚动发布>>小流量环境,开发测试及时跟进,观察异常情况,一旦碰到问题,第一时间关闭流量入口。相关问题定位debug可以在预发环境上进行>>所有发布到小流量环境的版本合集,通过一个晚高峰的检测后,发布到线上环境。第二天同样是做此循环,每天都是这样的发布模式。
31.jpg

有了流量染色以后,还可以得到单元化和多机房的染色。如果我们做高可用,至少需要两个机房,那么就存在一个问题,当一个机房完全挂了怎么办?微服务框架可以把它引流到另外一个机房。服务请求之后,还应该回来,因为应该本机房优先,毕竟本机房的容量大得多。所以我们建议整个部署模式,总分总的部署模式。

首先第一个总,要有统一的发布平台,无论发布到哪个Kubernetes,都应该通过一个平台。其次,你应该有一个多Kubernetes统一的管理,有多个机房,就有多个Kubernetes,我们并不建议跨机房。然后,我们建议应用层要有统一的视图,即使Kubernetes出现了问题,应用层可以把流量切到另外一个环境。就是这样一个总分总的模式。

另外Kubernetes也面临升级的问题,它更新比较快,经常升级。虽然业界有各种平滑的最佳实践,但是很难保证它升级的时候不出事。一旦Kubernetes出现状况,你也不想停里面的应用,可以采用分流的方式。
32.jpg

最终形成了云原生架构的技术栈,包括CICD、测试平台、容器平台、APM、分布式事务、微服务框架、API网关一栈式工具链。

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

    成熟电商背后落后的商品评价体系

    $
    0
    0

    疫情之下,受公共交通停运等因素影响,快递从业人员返岗困难,很多快递网点无法有效维持。但与之相应的是,线下实体商店商场迫于压力纷纷关门歇业,网购需求集中爆发。

    据国家邮政局统计,1月24日至29日,全国邮政业揽收包裹8125万件,同比增长76.6%;投递包裹7817万件,同比增长110.34%,整个快递行业面临着严峻考验,正在超负荷运转。

    传统电商受交通管制等因素影响,效率骤降。但新零售行业受相同因素的负面影响却要小得多,因此巨头们苦心布局多年的新零售,在当前算是迎来了难得的发展契机。

    据苏宁大数据显示,防疫期间,家乐福砂糖桔、肋排家庭装分别增长675%、1097%,菠菜更是暴增2628%,五连包方便面也暴增878%。苏宁小店订单量同比增长419.62%,其中白菜和土豆成为两大旺销菜品。

    其他新零售巨头同样生意火爆,以至于产生了严重的用工短缺问题,迫不得已开始从一些受疫情影响较大而歇业的行业,比如餐饮业、酒店业等借人或招人。

    截至2月9日,盒马方面已从餐饮业借用临时员工1600余人;美菜准备招聘4000名分拣员和6000个配送司机;京东物流开放了20000个岗位……新零售行业的火爆可见一斑。

    落后的商品评价系统,配不上面向未来的新零售

    当前的契机固然难得,但是新零售的火爆也并非全靠侥幸。

    2016年10月的阿里云栖大会上,马云在演讲中第一次提出了新零售,“未来的十年、二十年,没有电子商务这一说,只有新零售。”

    如今三年多的时间过去,马老师当初的预言,好像越来越成为一种明显的现实趋势。可以确定的是,疫情极大提升了新零售企业的市场渗透率。

    “在家里闷两周,把病毒憋死!”面对来势汹汹的新冠肺炎疫情,各大社交网络、媒体平台上,沸沸扬扬全是诸如此类的口号与决心。

    疫情之下,居家隔离无疑是最有效与最经济的切断感染、防控蔓延、自我保护的方式。然而,生活却不是如此简单。衣食住行中,行可以阻断,衣也可以暂时忽略,但食与住,也就是饮食与起居的必需品供应,就成了居家隔离中面临的最大挑战。

    新零售行业,正是居家隔离这道疫情防火线的核心支撑之一。

    从更朴素更本质的角度来看,以盒马鲜生为例,新零售目前主要扎堆集中在食品生鲜市场。习惯网上购物的年轻人们,本来并不是这一市场的主力军,但在当前,在被迫宅在家里的情况下,他们这方面的需求,在呈指数级暴涨;而许多原本转化困难的中老年人,此次也被迫转向线上买菜。

    在疫情过后,随着新零售渗透率的跨越式提高。很多人会对这种面向未来的零售新模式,产生更加深刻的认识。

    但是但是随着认识的深入,更多人会发现当前的新零售都有一个“小问题”,就是商品评价系统太过落后,根本配不上新零售这么富有远见的构想。

    新零售会对人、货、场的关系进行全面重构,效率提升和体验提升是显而易见的。但是我们如何能够把握好商品质量问题?又如何在商品质量和性价比之间找到一个很好的平衡点?

    目前的这种打分写评论的商品评价系统,在过往传统电商的发展过程中,已经暴露出越来越多的问题和缺陷。

    在些些问题都没有得到妥善解决以前,把这种商品评价系统和新零售进行简单粗暴的嫁接,明显不利于新零售行业更好更快发展。

    红包好评和刷单的根源

    在传统线下零售市场中,商品评价系统并不是一个值得探讨的问题。因为除了销售人员和亲友,其他人商品评价的影响力,可以说都是潜移默化的,很难对消费者决策产生直接影响。

    但是当电商浪潮开始涌起时,当电商交易规模占我国商品零售总额的比重越来越大时,电商平台的商品评价系统,就应该受到越来越多的重视。

    电子商务不是虚拟化的互联网衍生物,而是商品交易行为的在线化演变,尤其是牵扯到实物商品交易时,更是只有商品信息和支付手段是在线化的。

    在线支付问题,可以认为已经被支付宝和微信支付比较可靠的解决了;但是在线商品信息的可信度问题,到目前为止并没有产生一个非常合适的解决方案。之所以如此,可能还是这个问题本身难度就实在太高。

    无论是传统电商还是新零售,商品信息在线化和在线销售,都是首先环节,而消费者不能像线下那样直接接触商品,所以信息迷雾将会永远存在。

    在这个环节中,商家制造的信息迷雾越厚,就有可能获利越多;而消费者了解商品信息越全面,消除的信息迷雾越多,就有可能做出更合适的消费决策。这体现了买家和卖家之间存在的利益分歧。

    电商一开始的口号是“跳过中间商,让消费者直接受益”。但中间商其实从来没有真正的被电商平台摆脱过,他们只是在不断的变换形式,总是阴魂不散。带来的直接副作用就是,无论如何治理,电子商务的商品评价系统可信度,比起线下销售导购都并没有明显的提高。

    电商行业发展早期,假货问题和商品质量问题,深受消费者诟病,也成为线下商家攻讦的把柄。

    当电商行业发展越来越成熟之后,随着相关消费者保护法律法规的逐步完善,以及电商平台对劣质商家的清洗,平台的公信力开始逐步提高,口碑得到挽回。

    但商品评价系统并没有得到充分的重视和优化改良,所以尽管平台的打击力度不断加大,“刷单”和“发红包买好评”等行业乱象,非但没有得到有效遏制,反而愈演愈烈,直至成为行业潜规则,并遗毒至今。

    归根结底,在线了解商品信息,消费者只能通过商家展示和其他买家的评价这两种途径。商家信息展示,永远都不会是全面真实的,否则电商美工都要失业;而其他买家在道义上都应该真实评价,却没有真实评价的义务,因此其他买家的评价都是可疑的。

    正是因为存在这类问题,才会有了导购网站和电商直播的生存空间。

    导购网站兴衰

    电商平台不被信任,消费者自然就会求助于第三方,这首先催生出了导购网站的生存土壤。

    非典期间诞生了淘宝网,非典也启发了刘强东搞线上商城。但业界公认的“电子商务元年”是2010年。

    这一年中国网络购物市场年交易规模达到4610亿元,用户规模达到1.48亿。当当网赴美IPO,成为“中国电商赴美上市第一股”;苏宁易购、全新淘宝商城上线,国美以4800万元收购库巴网80%股份,全面进军电子商务。这时电子商务才算是完成了一定的市场教育,并且恰好赶上移动了互联网崛起的大趋势。

    同样也是在2010年前后,各类导购网站层出不穷。像我们耳熟能详的蘑菇街、美丽说、什么值得买都是在这一时期诞生的。

    2012年当聚美优品的陈欧喊出那些“我为自己代言”的极富煽动性的广告语时,蘑菇街和美丽说等返利导购网站已经把持着淘宝近10%的流量入口。

    这让阿里深感威胁,对此的反应非常激烈。2012年5月,马云在内部会议上发表了针对电商导购、返利类网站的几点原则:

    第一,不扶持上游导购网站继续做大,阿里的流量入口应该是草原而不是森林;

    第二,产业链上可以和异业合作,尽量不和同业合作;

    第三,不扶持返利类网站。

    蘑菇街因此不得不另想出路,2013年构建完成立足女性时尚领域的在线交易体系,实现了电商交易闭环。与蘑菇街非常相似的美丽说也在这一年做了类似的选择。

    从导购网站转型成电商平台,这种性质上的根本变化,像是一种轮回,也带来了蘑菇街们没有预料到过的困境。

    开放平台,让第三方商家入驻,商品品质就无法得到充分保证,口碑就会下降;专注于自营,商品质量提升,平台口碑有所回升,但是毛利润下降,发展扩张受限就会成为必然。

    这是所有电商平台都要经受的考验,但像蘑菇街这样从导购网站转型的电商,他们身上背负的枷锁自然会更重一些,所以导购网站转型而来的电商平台,都总是在昙花一现之后就会迅速沉寂下去。

    也有不做电商的导购网站,比如什么值得买,但是像这样纯粹的导购网站,随着发展扩张,对于商品广告的依赖性会不断提高,用户群体从小众扩展到大众,口碑下跌,发展受限同样也是不可避免的。

    归根结底,其实对于“商品评价真实性问题”,导购网站并不是理想的解决方案。因为根据市场经济发展规律,在口碑和收益之间,导购网站最后不可避免的会都会倒向收益。而做不做电商,只是表象,最大的问题在于,为了收益,失去了客观性,导购网站的评论就不再可信。

    畸形的电商直播

    2019年电商行业最大的变数,是网红电商直播的崛起。而相比起导购网站,电商直播的本质更畸形。

    从商品评价的角度来看,网红电商直播的本质,就是通过直播这种更直观、互动性更强的表现形式,利用网红的商品评价,引导消费者消费。

    问题是网红凭什么对商品信息的真实性背书?

    无论是传统电商还是新零售,线上零售说到底都是商品交易,电商直播固然有一定的娱乐性,能值一些回票价,但是绝大多数消费者网购的根本目的,都是买到符合需求的商品,而网红并不能保证消费者可以买到货真价实的商品。

    2019年是很多媒体口中的“电商直播元年”。

    阿里官方数据显示,2019年超过50%的商家都在双十一当天开启了直播间。双十一开场仅1小时03分,直播引导的成交就超过2018年双十一全天;8小时55分,淘宝直播大盘上的引导成交规模破百亿,其中在家装和消费电子等行业,直播引导的成交同比增长均超过400%。

    国盛证券有过预计,正式货币化之后,淘宝直播变现率将达淘宝天猫目前变现水平的数倍,并有望贡献阿里国内零售业务2020财年和2021财年收入增量的15%~18%。

    看起来电商直播的前景确实值得期待,但电商直播爆火的同时,也伴随着层出不穷的直播翻车事故。

    2019年最受关注的网红,毫无疑问就是李佳琪。“为用户考虑、敬业、态度诚恳”等是粉丝为他贴上的标签,在“铁锅”“大闸蟹”等翻车事件之后,李佳琪后续也确实做出了验证评测和诚恳道歉。

    但问题的根源在于,无论专不专业,网红用自己的个人信誉给商品背书,出了问题都很难保障消费者的合法权益。

    在2019年双十一过后,商务部发言人高峰曾在发布会上表示,关于网络直播促销,作为一种新兴的电子商务营销模式,可以帮助消费者提升消费体验,为许多质量有保证、服务有保障的产品打开销路。

    同时任何业态模式的运行都必须符合有关法律法规,必须保障消费者的合法权益,将继续会同有关部门一道,推动电子商务的规范化发展,切实维护电子商务市场的秩序和广大消费者的合法权益。

    保障消费者合法权益,健全的法律法规是最有效的。但无论如何,直播网红和商品商家加强自律同样很关键。

    熟人评价系统的构想

    传统的打分评价、导购网站、电商直播,这些都是中国电商行业发展各个阶段,由各种因素共同影响而产生的商品评价机制。但从社交关系的角度看,他们都属于陌生人商品评价系统。

    可以这样理解,在天猫、京东这类传统电商平台中,买家和买家之间的关系是完全的陌生人;在导购网站中,买家之间的关系是内容社区的帖主和帖友;在电商直播中,参与双方的定位既包括销售员和消费者,也包括网红和粉丝。但无论看起来有多么复杂,删繁就简之后就可以轻松看出,这些都是弱社交关联性关系,统统可以被归结为陌生人。

    而陌生人与陌生人之间,并不存在天然的信任基础,因为彼此之间并没有说真话的义务。但是在熟人关系之间,情况则大不相同。

    在绝大部分时间里,我们和亲人朋友之间的利益都是一致的,彼此之间有说真话的义务。这意味着,从根源上讲,我们有充分的动机把好东西推荐亲朋好友,或者把一个东西的真实信息展示给亲朋好友,让他们不吃亏。

    所以,基于熟人社交关系的商品评价系统,天然比陌生人商品评价系统更加值得信赖。

    近日,拼多多宣布将对商品评价系统进行调整升级,严打虚假好评。升级后,在原有标签评价系统之外,拼多多将允许用户自主向主动分享过拼团信息的好友公开自己的评价内容。

    这看起来像是对熟人商品评价系统的一次实践,拼多多自己也是这么宣传的。

    在拼多多年活跃买家超过5亿,月活跃用户数量达到4亿多的今天,拼多多的拼团活动中,亲友拼团的比例,早已经被高频率的陌生人拼团稀释的可以忽略不计了。

    这无疑是一次有意义的尝试,但也有它的局限性。

    拼团信息大概率会被分享在拼团群里,而不是亲友群里,这是一个本质上的差别。因此可以断言,这并不是真正意义上的熟人商品评价系统,这种尝试对信任成本的削减,并不会取得立竿见影的效果。

    但是更好的熟人评价系统应该是什么样的?这其实也是很难实现的突破。

    商品交易的根基是信任

    所有人都知道要“诚信为本”,但是最简单的道理,总是最难贯彻到底。

    1月17日消息,据国家统计局数据,2019年社会消费品零售总额411649亿元,全国网上零售额106324亿元,比上年增长16.5%。网上零售的重要地位日益彰显,但是线上商品评价系统依然远远称不上成熟。

    在线支付系统的搭建和成熟,已经基本解决了网上零售的信用问题,并且反攻线下,大街小巷贴满的二维码,让交易行为的高效便捷,有了质的提高。

    那么线上商品交易的信任问题又该怎样解决?

    相信随着技术的不断发展进步,5G、AI、AR、VR等先进技术在线上零售行业中的普及应用,商品信息展示的直观性和全面性会大大提高。

    但是不能第一时间直接接触到实物,线上交易的信任成本,天然还是要比线下交易更高。如果不能通过有效的机制让商家更主动的展示真实信息,不能让商品评价系统更加真实可信,技术进步,也并不能有效改善线上零售真正的薄弱环节。

    商家和平台要想降低消费者的不信任感,自身的自律和诚信是根本。同时也应该做出更多的尝试,尤其是要立足于消费者的根本需求,构建出更加合理、有效、可信的商品评价系统。

    文:刘旷公众号,ID:liukuang110


    Elasticsearch调优篇-慢查询分析笔记 - 个人文章 - SegmentFault 思否

    $
    0
    0

    前言

    • elasticsearch提供了非常灵活的搜索条件给我们使用,在使用复杂表达式的同时,如果使用不当,可能也会为我们带来了潜在的风险,因为影响查询性能的因素很多很多,这篇笔记主要记录一下慢查询可能的原因,及其优化的方向。
    • 本文讨论的es版本为7.0+。

    慢查询现象

    查询服务超时
    • 最直观的现象就是提供查询的服务响应超时。
    大量连接被拒绝

    file

    • 我们有时候写查询,为了图方遍,经常使用通配符*来查询,这有可能会匹配到多个索引,由于索引下分片太多,超过了集群中的核心数。就会在搜索线程池中造成排队任务,从而导致搜索拒绝。
    查询延迟
    主机CPU飙高
    • 另一个常见原因是磁盘 I/O 速度慢,导致搜索排队或在某些情况下 CPU 完全饱和。
    • 除了文件系统缓存,Elasticsearch 还使用查询缓存和请求缓存来提高搜索速度。 所有这些缓存都可以使用搜索请求进行优化,以便每次都将某些搜索请求路由到同一组分片,而不是在不同的可用副本之间进行交替。这将更好地利用请求缓存、节点查询缓存和文件系统缓存。Es默认会在内存使用75%时发生FullGC ,做好主机和节点的监控同样重要。

    file

    file

    优化方法
    根据查询时间段动态计算索引
    • elasticsearch支持同时查询多个索引,为了提高查询效率,避免使用通配符查询,我们可以计算枚举出所有的目标索引,一般es的数据都是按时间分索引,我们可以根据前端传入的时间段,计算出目标索引。
    控制分片数量
    • 分片的数量和节点和内存有一定的关系。
    • 最理想的分片数量应该依赖于节点的数量。 数量是节点数量的1.5到3倍。
    • 每个节点上可以存储的分片数量,和堆内存成正比。官方推荐:1GB 的内存,分片配置最好不要超过20。
    注意from/to查询带来的深度分页问题
    • 举例假如每页为 10 条数据,你现在要查询第 200 页,实际上是会把每个 Shard 上存储的前 2000条数据都查到一个协调节点上。

    如果你有 5 个 分片,那么就有 10000 条数据,接着协调节点对这 10000 条数据进行一些合并、处理,再获取到最终第 200 页的 10 条数据。实在需要查询很多数据,可以使用scroll API 滚动查询。

    为你的索引配置索引模板
    • 在低版本的es中默认的分片是5个,在高版本中改成了1,我们需要根据索引的索引量来动态调整分片数量,这里推荐设置一个默认匹配规则,将优先级设置高一些(ps:order高的会覆盖order低的模板),避免查询扫描过多的分片,合理利用集群资源。

    file

    避免数据分桶太多

    对于分桶数量太大的聚合请求,应该将所有数据切片,比如按时间分片,多次请求,来提高查询效率,并且避免内存OOM。

    独立协调节点
    • 集群中应该有独立的协调节点,专门用于数据请求(node.master=false node.data=false),并给它们设置足够的内存。通过数据节点与协调节点分离,可以避免节点挂掉之后,导致整个集群不可用,或者长时间响应迟钝。
    Routing数据路由
    适当的增加刷新间隔
    • es是一个准实时的搜索框架,这就意味着,从索引一个文档直到文档能够被搜索到有一个轻微的延迟,也就是 index.refresh_ interval ,默认值是1秒,适当的增加这个值,可以避免创建过多的segment(segment是最小的检索单元)。
    配置慢查询日志
    • 通过在 Elasticsearch 中启用 slowlogs 来识别运行缓慢的查询。slowlogs 专门用于分片级别,仅适用于数据节点。协调/客户端节点不具备慢日志分析功能,因为它们不保存数据。通过它,我们可以在日志中看到,那个查询语句耗时长,从而制定优化措施。
    index.search.slowlog.threshold.query.warn: 10s
    index.search.slowlog.threshold.query.info: 5s
    index.search.slowlog.threshold.query.debug: 2s
    index.search.slowlog.threshold.query.trace: 500ms
    
    index.search.slowlog.threshold.fetch.warn: 1s
    index.search.slowlog.threshold.fetch.info: 800ms
    index.search.slowlog.threshold.fetch.debug: 500ms
    index.search.slowlog.threshold.fetch.trace: 200ms
    
    index.search.slowlog.level: info
    配置熔断策略
    • es7.0后版本提供一系列的断路器,用于防止操作引起OutOfMemoryError。每个断路器都指定了可以使用多少内存的限制。此外,还有一个父级断路器,用于指定可在所有断路器上使用的内存总量。

    indices.breaker.request.limit:请求中断的限制,默认为JVM堆的60%。

    indices.breaker.total.limit:总体父中断程序的启动限制,如果indices.breaker.total.use_real_memory为,则默认为JVM堆的70% false。如果indices.breaker.total.use_real_memory 为true,则默认为JVM堆的95%。

    network.breaker.inflight requests.limit 限制当前通过HTTP等进来的请求使用内存不能超过Node内存的指定值。这个内存主要是限制请求内容的长度。 默认100%。

    script.max_compilations_rate:在允许的时间间隔内限制动态脚本的并发执行数量。默认值为75 / 5m,即每5分钟75。

    欢迎来公众号【侠梦的开发笔记】 一起交流进步

    Mysql 高可用架构 - 沐沐爸比的个人空间 - OSCHINA

    $
    0
    0

    主从复制模式

    从服务器拉取主服务器上拉取执行的日志binlog文件,同步进行执行保持数据一致;可通过keepalived 方式保证服务无间断可用;
    如果db中一台机器故障后,则可发送请求到正常的机器上,实现了高可用;所有db都是可读写;
    缺点:主从复制过程无监控,意外中断则会导致数据丢失不一致的情况。

     

    MMM方案

    即使主从复制中间出现异常情况,MMM记录了详细同步日志,重启服务后会从错误的地方继续同步,不会丢失数据;

    Mysql经典架构方案

    此方案做了      读写分离,写入是通过访问映射VIP地址,写入到主服务器,再通过vip实现主从同步保持数据一致;      LVS+keppalied实现读从数据库的负载均衡;从lvs1和lvs2请求到 dbs159,dbs160,dbs161;      Heartbeat+DRBD实现主从复制,自动故障切换,IP自动漂移;

    微服务架构:如何用十步解耦你的系统?

    $
    0
    0

    导言

    耦合性,是对模块间关联程度的度量。耦合的强弱取决于模块间接口的复杂性、调用模块的方式以及通过界面传送数据的多少。模块间的耦合度是指模块之间的依赖关系,包括控制关系、调用关系、数据传递关系。模块间联系越多,其耦合性越强,同时表明其独立性越差。软件设计中通常用耦合度和内聚度作为衡量模块独立程度的标准。高内聚低耦合,是软件工程中的概念,是判断设计好坏的标准,主要是面向对象的设计,主要是看类的内聚性是否高,耦合度是否低。

    首先献上微服务的技术点思维导图:

    SpringCloud和Dubbo都是现在比较成熟的微服务框架,如何使用两者构建搭建你的微服务系统呢?他们是如何将你的系统解耦的?又是怎么解耦的呢?请听我慢慢道来:

    第一步,解耦现有模块

    将现有耦合在一起的模块进行重新的设计,设计成可以独立部署的多个模块,使用微服务框架很容易做到,成熟的示例代码都特别多,这里不再多讲。下面是我的微服务实现的一个架构设计图。

    第二步,抽取公共模块

    架构设计原则之一就是反向依赖,只从上往下依赖,所以,我们将公共的重复功能的模块抽取出来。必须强调一点的是,公共模块必须足够的功能单一,不能有其他业务的逻辑判断在里面。在整个模块依赖关系里,应该是一棵树状结构的关系图,而不是一个网状的关系图。

    1)做好代码控制

    笔者之前就碰到过这种问题,模块划分完了,当需求变更的时候,研发人员根本不管是不是公共模块,只要能快速完成任务,哪里改的快就在哪里改。因此,这个需要内部要做好代码的权限管理,不应该开放所有的提交代码的权限给所有的人。后来我就将公共模块的合并代码的权限收回了,合并代码需要先提交申请,代码review过才能合并代码。这就保证了公共模块代码的功能单一。

    2)做好版本管理

    公共模块被多个模块模块使用,任何代码的修改都可能会导致到正在使用的模块无法使用。这个就需要做好各个模块的版本管理,我是使用maven进行版本管理的,定义一个总的父pom项目来进行各个模块的版本管理,任何被其他模块使用的开发包都要在父pom里进行版本管理。当新的需求来了以后,需要对公共模块进行修改时,要更新模块的版本号,同时更新父pom的版本号,需要使用公共模块新功能的模块就修改父pom的版本号,不需要使用公共模块新功能的模块就不用修改父pom的版本号,这样公共模块的新老版本都能使用,即使出现问题,也只会影响到使用新版本的模块。

    第三步,解耦迭代需求

    现在的代码迭代速度快,同时会面对多个需求,有的需求紧急,有的需求不紧急,而且紧急程度可能随时会调整,如果将所有的需求都放在一个分支,当只想上线其中几个需求的时候发现无法将不上线需求的代码拆分出来,是不是很尴尬,即使能拆分出来,代码修改过以后又要重新进行部署测试,很费时费力,所以要针对不同的需求重新建立研发分支,这样就将不同需求的分支解耦,保证想上哪个就上哪个,需要上多个需求的就将分支合并上线。

    第四步,配置解耦

    为每个模块每个环境配置一个配置文件,这样就可以把不同的环境的配置解耦,不用每次上线都更新一次。但是如果需要修改数据库配置,还是需要重新部署重启应用才能解决。使用微服务的配置中心就能解决这个问题了,比如使用ZooKeeper作为SpringCloud的配置中心,修改ZooKeeper中的节点数据就可以实时更新配置并生效。

    第五步,权限解耦

    当采用微服务架构把原来的系统拆分成多个系统以后,你会发现原来简单的问题,现在变的复杂了,比如功能的权限控制,原来是跟业务代码放到一起,现在如果每个业务模块都有功能权限的代码,将是一件非常麻烦的事情。那么解决办法就是将权限功能迁移出来,恰巧使用SpringCloudGateway就能完成这件事情,SpringCloudGateway能够进行负载均衡,各种路由拦截,只要将原来的权限控制代码迁移到Gateway里实现以下就可以了,权限配置管理界面和代码逻辑都不用变。如果是API接口呢,就需要将安全验证等功能放在Gateway里实现就好了。

    第六步,流量解耦

    当你的系统访问量越来越大的时候,你会发现每次升级都是一件非常麻烦的事情,领导会跟你说这个功能忙时不能停机影响用户使用呀,只能半夜升级呀,多么痛快的事情啊。有的时候运营人员也会发现,怎么我的后台访问怎么这么慢?问题出在哪里呢?问题就出在,所有的模块都用了一个Gateway,多端同时使用了相同的流量入口,当在举行大促时,并发量非常高,带宽占用非常大,那么其他的功能也会跟着慢下来。

    不能在举行大促时发券时,我线下支付一直支付不了,这是非常严重的事故了,客服电话会被打爆了。所以,必须要对流量进行拆分,各个端的流量不能相互影响,比如APP端、微信端、运营后台和商户后台等都要分配独立的Gateway,并接入独立的带宽,对于流量大的端可以使用弹性带宽,对于运营后台和商户后台就比较小的固定的带宽即可。这样就大大降低了升级时的难度,是不是再上线时就没那么紧张了?

    第七步,数据解耦

    系统刚上线的时候,数据量不大,所有的模块感觉都挺好的,当时间一长,系统访问量非常大的时候会发现功能怎么都变慢了,怎么mysql的cpu经常100%。那么恭喜你,你中招了,你的数据需要解耦了。

    首先要模块间数据解耦,将不同模块使用独立的数据库,保证各模块之间的数据不相互影响。

    其次就是冷热数据解耦,同一个模块运行时间长了以后也会积累大量的数据,为了保证系统的性能的稳定,要减少因为数据量太大造成的性能降低,需要对历史数据进行定期的迁移,对于完整数据分析汇总就在其他的库中实现。

    第八步,扩容解耦

    一个好的架构设计是要有好的横向扩展的能力,在不需要修改代码只通过增加硬件的方式就能提高系统的性能。SpringCloud和Dubbo的注册中心天生就能够实现动态添加模块的节点,其他模块调用能够实时发现并请求到新的模块节点上。

    第九步,部署解耦

    互联网开发在于能够快速的试错,当一个新的版本上线时,经常是需要先让一部分用户进行测试一下,这就是传说中的灰度发布,同一个模块先部署升级几台服务器到新版本,重启完成后流量进来以后,就可以验证当前部署的这几台服务器有没有问题,就继续部署其他的节点,如果有问题马上回滚到上一个版本。使用SpringCloudGateway的WeighRouterFilter就能实现这个功能。

    第十步,动静解耦

    当同一个模块的瞬间有非常高并发的时候,对,就是说的秒杀,纯粹的流量解耦还是不够,因为不能让前面的流量冲击后面真正的下单的功能,这个时候就需要更细的流量解耦,要将静态文件的访问通通抛给CDN来解决,动态和静态之间是通过定时器来出发的,时间未到之前一直刷新的是静态的文件,当时间到了之后,生成新的js文件,告诉静态页面可以访问下单功能了。

    总结

    在模块划分时,要遵循“一个模块,一个功能”的原则,尽可能使模块达到功能内聚。

    事实上,微服务架构短期来看,并没有很明显的好处,甚至短期内会影响系统的开发进度,因为高内聚,低耦合的系统对开发设计人员提出了更高的要求。高内聚,低耦合的好处体现在系统持续发展的过程中,高内聚,低耦合的系统具有更好的重用性,维护性,扩展性,可以更高效的完成系统的维护开发,持续的支持业务的发展,而不会成为业务发展的障碍。

    最后

    欢迎大家关注我新开通的公众号【 风平浪静如码】,最新最全多家公司java面试题整理了1000多道400多页pdf文档,文章都会在里面更新,整理的资料也会放在里面。

    喜欢文章记得关注我点个赞哟,感谢支持!

    使用 Docker 五分鐘安裝好 Gitea (自架 Git Hosting 最佳選擇)

    $
    0
    0

    Gitea

    Gitea在本週發佈了 1.11.0 版本,本篇就使用 Docker 方式來安裝 Gitea,執行時間不會超過五分鐘。Gitea 是一套開源的 Git Hosting,除了 Gitea 之外,您可以選擇 GitHub 或自行安裝 GitLab,但是我為什麼選擇 Gitea 呢?原因有底下幾點

    1. Gitea 是 開源專案,全世界的開發者都可以進行貢獻
    2. Gitea 是 Go 語言所開發,啟動速度超快
    3. Gitea 開源社區非常完整,每年固定挑選三位為主要負責人
    4. Gitea 可以使用執行檔或 Docker 方式進行安裝

    Gitea 目前發展方向就是自己服務自己,大家可能有發現原本在 GitHub 上面的 Repository 已經全面轉到 Gitea 自主服務了,這也代表著未來會全面轉過去,只是時間上的問題。Gitea 目前的功能其實相當完整,大家有興趣可以看這張 比較表,新創團隊我都強烈建議使用 Gitea。

    教學影片

    如果對於課程內容有興趣,可以參考底下課程。

    安裝方式

    透過 docker-compose 方式安裝會是最快的,大家可以參考 此 Repository

    version: "2"
    
    networks:
      gitea:
        external: false
    
    services:
      server:
        image: gitea/gitea:1.11.0
        environment:
          - USER_UID=${USER_UID}
          - USER_GID=${USER_GID}
          - SSH_PORT=2000
          - DISABLE_SSH=true
          - DB_TYPE=mysql
          - DB_HOST=db:3306
          - DB_NAME=gitea
          - DB_USER=gitea
          - DB_PASSWD=gitea
        restart: always
        networks:
          - gitea
        volumes:
          - gitea:/data
          - /etc/timezone:/etc/timezone:ro
          - /etc/localtime:/etc/localtime:ro
        ports:
          - "4000:3000"
          - "2000:22"
    
      db:
        image: mysql:5.7
        restart: always
        environment:
          - MYSQL_ROOT_PASSWORD=gitea
          - MYSQL_USER=gitea
          - MYSQL_PASSWORD=gitea
          - MYSQL_DATABASE=gitea
        networks:
          - gitea
        volumes:
          - mysql:/var/lib/mysql
    
    volumes:
      gitea:
        driver: local
      mysql:
        driver: local

    由上面可以看到只有啟動 Gitea + MySQL 服務就完成了,啟動時間根本不用 10 秒鐘,打開瀏覽器就可以看到安裝畫面了。

    SpringCloud灰度发布实践(附源码) - 微服务实践 - SegmentFault 思否

    $
    0
    0

    代码GIT

    前言

    ​ 在平时的业务开发过程中,后端服务与服务之间的调用往往通过 fegin或者 resttemplate两种方式。但是我们在调用服务的时候往往只需要写服务名就可以做到路由到具体的服务,这其中的原理相比大家都知道是 SpringCloudribbon组件帮我们做了负载均衡的功能。

    灰度的核心就是路由,如果我们能够重写ribbon默认的负载均衡算法是不是就意味着我们能够控制服务的转发呢?是的!

    调用链分析

    外部调用

    • 请求==>zuul==>服务
    zuul在转发请求的时候,也会根据 Ribbon从服务实例列表中选择一个对应的服务,然后选择转发.

    内部调用

    • 请求==>zuul==>服务Resttemplate调用==>服务
    • 请求==>zuul==>服务Fegin调用==>服务
    无论是通过 Resttemplate还是 Fegin的方式进行服务间的调用,他们都会从 Ribbon选择一个服务实例返回.

    上面几种调用方式应该涵盖了我们平时调用中的场景,无论是通过哪种方式调用(排除直接ip:port调用),最后都会通过 Ribbon,然后返回服务实例.

    预备知识

    eureka元数据

    Eureka的元数据有两种,分别为标准元数据和自定义元数据。

    标准元数据:主机名、IP地址、端口号、状态页和健康检查等信息,这些信息都会被发布在服务注册表中,用于服务之间的调用。

    自定义元数据:自定义元数据可以使用 eureka.instance.metadata-map配置,这些元数据可以在远程客户端中访问,但是一般不会改变客户端的行为,除非客户端知道该元数据的含义

    eureka RestFul接口

    请求名称请求方式HTTP地址请求描述
    注册新服务POST/eureka/apps/ {appID}传递JSON或者XML格式参数内容,HTTP code为204时表示成功
    取消注册服务DELETE/eureka/apps/ {appID}/ {instanceID}HTTP code为200时表示成功
    发送服务心跳PUT/eureka/apps/ {appID}/ {instanceID}HTTP code为200时表示成功
    查询所有服务GET/eureka/appsHTTP code为200时表示成功,返回XML/JSON数据内容
    查询指定appID的服务列表GET/eureka/apps/ {appID}HTTP code为200时表示成功,返回XML/JSON数据内容
    查询指定appID&instanceIDGET/eureka/apps/ {appID}/ {instanceID}获取指定appID以及InstanceId的服务信息,HTTP code为200时表示成功,返回XML/JSON数据内容
    查询指定instanceID服务列表GET/eureka/apps/instances/ {instanceID}获取指定instanceID的服务列表,HTTP code为200时表示成功,返回XML/JSON数据内容
    变更服务状态PUT/eureka/apps/ {appID}/ {instanceID}/status?value=DOWN服务上线、服务下线等状态变动,HTTP code为200时表示成功
    变更元数据PUT/eureka/apps/ {appID}/ {instanceID}/metadata?key=valueHTTP code为200时表示成功

    更改自定义元数据

    配置文件方式:

    eureka.instance.metadata-map.version = v1

    接口请求:

    PUT                  /eureka/apps/{appID}/{instanceID}/metadata?key=value

    实现流程

    灰度设计

    原图链接

    1. 用户请求首先到达Nginx然后转发到网关 zuul,此时 zuul拦截器会根据用户携带请求 token解析出对应的 userId
    2. 网关从Apollo配置中心拉取灰度用户列表,然后根据灰度用户策略判断该用户是否是灰度用户。如是,则给该请求添加 请求头线程变量添加信息 version=xxx;若不是,则不做任何处理放行
    3. zuul拦截器执行完毕后, zuul在进行转发请求时会通过负载均衡器Ribbon。
    4. 负载均衡Ribbon被重写。当请求到达时候,Ribbon会取出 zuul存入 线程变量version。于此同时,Ribbon还会取出所有缓存的服务列表(定期从eureka刷新获取最新列表)及其该服务的 metadata-map信息。然后取出服务 metadata-mapversion信息与线程变量 version进行判断对比,若值一直则选择该服务作为返回。若所有服务列表的version信息与之不匹配,则返回null,此时Ribbon选取不到对应的服务则会报错!
    5. 当服务为非灰度服务,即没有version信息时,此时Ribbon会收集所有非灰度服务列表,然后利用Ribbon默认的规则从这些非灰度服务列表中返回一个服务。


    6. zuul通过Ribbon将请求转发到consumer服务后,可能还会通过 feginresttemplate调用其他服务,如provider服务。但是无论是通过 fegin还是 resttemplate,他们最后在选取服务转发的时候都会通过 Ribbon
    7. 那么在通过 feginresttemplate调用另外一个服务的时候需要设置一个拦截器,将请求头 version=xxx给带上,然后存入线程变量。
    8. 在经过 feginresttemplate的拦截器后最后会到Ribbon,Ribbon会从 线程变量里面取出 version信息。然后重复步骤(4)和(5)

    灰度流程

    设计思路

    首先,我们通过更改服务在eureka的元数据标识该服务为灰度服务,笔者这边用的元数据字段为 version

     

    1.首先更改服务元数据信息,标记其灰度版本。通过eureka RestFul接口或者配置文件添加如下信息 eureka.instance.metadata-map.version=v1

    2.自定义 zuul拦截器 GrayFilter。此处笔者获取的请求头为token,然后将根据JWT的思想获取userId,然后获取灰度用户列表及其灰度版本信息,判断该用户是否为灰度用户。

    若为灰度用户,则将灰度版本信息 version存放在线程变量里面。 此处不能用 Threadlocal存储线程变量,因为SpringCloud用hystrix做线程池隔离,而线程池是无法获取到ThreadLocal中的信息的!所以这个时候我们可以参考 Sleuth做分布式链路追踪的思路或者使用阿里开源的 TransmittableThreadLocal方案。此处使用 HystrixRequestVariableDefault实现跨线程池传递线程变量。

    3.zuul拦截器处理完毕后,会经过ribbon组件从服务实例列表中获取一个实例选择转发。Ribbon默认的 Rule为ZoneAvoidanceRule`。而此处我们继承该类,重写了其父类选择服务实例的方法。

    以下为Ribbon源码:

    public abstract class PredicateBasedRule extends ClientConfigEnabledRoundRobinRule {
       // 略....
        @Override
        public Server choose(Object key) {
            ILoadBalancer lb = getLoadBalancer();
            Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
            if (server.isPresent()) {
                return server.get();
            } else {
                return null;
            }       
        }
    }

    以下为自定义实现的伪代码:

    public class GrayMetadataRule extends ZoneAvoidanceRule {
       // 略....
        @Override
        public Server choose(Object key) {
          //1.从线程变量获取version信息
            String version = HystrixRequestVariableDefault.get();
            
          //2.获取服务实例列表
            List<Server> serverList = this.getPredicate().getEligibleServers(this.getLoadBalancer().getAllServers(), key);
           //3.循环serverList,选择version匹配的服务并返回
                    for (Server server : serverList) {
                Map<String, String> metadata = ((DiscoveryEnabledServer) server).getInstanceInfo().getMetadata();
    
                String metaVersion = metadata.get("version);
                if (!StringUtils.isEmpty(metaVersion)) {
                    if (metaVersion.equals(hystrixVer)) {
                        return server;
                    }
                }
            }
        }
    }

    4.此时,只是已经完成了 请求==》zuul==》zuul拦截器==》自定义ribbon负载均衡算法==》灰度服务这个流程,并没有涉及到 服务==》服务的调用。

    服务到服务的调用无论是通过resttemplate还是fegin,最后也会走ribbon的负载均衡算法,即 服务==》Ribbon 自定义Rule==》服务。因为此时自定义的 GrayMetadataRule并不能从线程变量中取到version,因为已经到了另外一个服务里面了。

    5.此时依然可以参考 Sleuth的源码 org.springframework.cloud.sleuth.Span,这里不做赘述只是大致讲一下该类的实现思想。 就是在请求里面添加请求头,以便下个服务能够从请求头中获取信息。

    此处,我们可以通过在 步骤2中,让zuul添加添加线程变量的时候也在请求头中添加信息。然后,再自定义 HandlerInterceptorAdapter拦截器,使之在到达服务之前将请求头中的信息存入到线程变量HystrixRequestVariableDefault中。

    然后服务再调用另外一个服务之前,设置resttemplate和fegin的拦截器,添加头信息。

    resttemplate拦截器

    public class CoreHttpRequestInterceptor implements ClientHttpRequestInterceptor {
        @Override
        public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
            HttpRequestWrapper requestWrapper = new HttpRequestWrapper(request);
            String hystrixVer = CoreHeaderInterceptor.version.get();
            requestWrapper.getHeaders().add(CoreHeaderInterceptor.HEADER_VERSION, hystrixVer);
            return execution.execute(requestWrapper, body);
        }
    }

    fegin拦截器

    public class CoreFeignRequestInterceptor implements RequestInterceptor {
       @Override
       public void apply(RequestTemplate template) {
            String hystrixVer = CoreHeaderInterceptor.version.get();
            logger.debug("====>fegin version:{} ",hystrixVer); 
          template.header(CoreHeaderInterceptor.HEADER_VERSION, hystrixVer);
       }
    
    }

    6.到这里基本上整个请求流程就比较完整了,但是我们怎么让Ribbon使用 自定义的Rule?这里其实非常简单,只需要在服务的配置文件中配置一下代码即可.

    yourServiceId.ribbon.NFLoadBalancerRuleClassName=自定义的负载均衡策略类

    但是这样配置需要指定服务名,意味着需要在每个服务的配置文件中这么配置一次,所以需要对此做一下扩展.打开源码 org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration类,该类是Ribbon的默认配置类.可以清楚的发现该类注入了一个 PropertiesFactory类型的属性,可以看到 PropertiesFactory类的构造方法

    public PropertiesFactory() {
            classToProperty.put(ILoadBalancer.class, "NFLoadBalancerClassName");
            classToProperty.put(IPing.class, "NFLoadBalancerPingClassName");
            classToProperty.put(IRule.class, "NFLoadBalancerRuleClassName");
            classToProperty.put(ServerList.class, "NIWSServerListClassName");
            classToProperty.put(ServerListFilter.class, "NIWSServerListFilterClassName");
        }

    所以,我们可以继承该类从而实现我们的扩展,这样一来就不用配置具体的服务名了.至于Ribbon是如何工作的,这里有一篇方志明的文章(传送门)可以加强对Ribbon工作机制的理解

    7.到这里基本上整个请求流程就比较完整了,上述例子中是以用户ID作为灰度的维度,当然这里可以实现更多的灰度策略,比如IP等,基本上都可以基于此方式做扩展

    灰度使用

    配置文件示例

    spring.application.name = provide-test
    server.port = 7770
    eureka.client.service-url.defaultZone = http://localhost:1111/eureka/
    
    #启动后直接将该元数据信息注册到eureka
    #eureka.instance.metadata-map.version = v1

    测试案例

    ​ 分别启动四个测试实例,有version代表灰度服务,无version则为普通服务。当灰度服务测试没问题的时候,通过PUT请求eureka接口将version信息去除,使其变成普通服务.

    实例列表

    • [x] zuul-server
    • [x] provider-test
      port:7770 version:无
      port: 7771 version:v1
    • [x] consumer-test

      port:8880 version:无

      port: 8881 version:v1

    修改服务信息

    ​ 服务在eureka的元数据信息可通过接口 http://localhost:1111/eureka/apps访问到。

    服务信息实例:

    访问接口查看信息 http://localhost:1111/eureka/apps/PROVIDE-TEST

    服务info信息

    注意事项

    ​ 通过此种方法更改server的元数据后,由于ribbon会缓存实力列表,所以在测试改变服务信息时,ribbon并不会立马从eureka拉去最新信息m,这个拉取信息的时间可自行配置。

    同时,当服务重启时服务会重新将配置文件的version信息注册上去。

    测试演示

    zuul==>provider服务

    用户andy为灰度用户。
    1.测试灰度用户andy,是否路由到灰度服务 provider-test:7771
    2.测试非灰度用户andyaaa(任意用户)是否能被路由到普通服务 provider-test:7770

    zuul-服务

    zuul==>consumer服务>provider服务

    以同样的方式再启动两个consumer-test服务,这里不再截图演示。

    请求从zuul==>consumer-test==>provider-test,通过 feginresttemplate两种请求方式测试

    Resttemplate请求方式

    zuul-服务-resttemplate服务

    fegin请求方式

    zuul-服务-fegin

    自动化配置

    与Apollo实现整合,避免手动调用接口。实现配置监听,完成灰度。详情见 下篇文章

    Spring Cloud版本控制和灰度starter-spring-cloud-gray

    $
    0
    0

    Spring Cloud Gray - 微服务灰度中间件

    LicenseA ReleaseB ReleaseB Release

    介绍

    Spring Cloud Gray 是一套开源的微服务灰度路由解决方案,它由spring-cloud-gray-client,spring-cloud-gray-client-netflix 和 spring-cloud-tray-server,spring-cloud-gray-webui组成。
    spring-cloud-gray-client定义了一套灰度路由决策模型,灰度信息追踪模型,以及和spring-cloud-gray-server的基本通信功能。
    spring-cloud-gray-client-netflix在spring-cloud-gray-client的基础上集成了微服务注册中心eureka,扩展ribbon的负载均衡规则,提供了对zuul,feign,RestTemplate的灰度路由能力,并且无缝支持hystrix线程池隔离。
    spring-cloud-gray-server负责灰度决策、灰度追踪等信息的管理以及持久化。
    spring-cloud-gray-webui提供操作界面。

    钉钉交流群

    Demo

    点击查看

    在管控端数据库执行一条insert语名:

    insert into`user`(`user_id`,`account`,`name`,`password`,`roles`,`status`,`create_time`,`operator`,`operate_time`)values('admin','admin','Admin','e7a57e51394e91cba19deca3337bfab0','admin','1', now(),'admin', now());

    这是添加管理员账号的,用户名: admin密码: abc123

    使用手册

    管控端部署手册
    客户端部署手册
    管控端配置参数说明
    客户端配置参数说明
    管控端界面使用手册

    版本信息

    项目分支srpingcloud版本springboot版本
    AEdgware1.5.*
    BFinchley2.0.*
    CGreenwich2.1.*

    Spring Cloud Gray 能做什么

    1. 金丝雀测试

      先发布1台实例,用于测试验证,指定测试的流量进入这台实例,其它流量依然进入其它正常的实例。优势在于发布成本小,快速测试,并且不影响正常用户体验影响,即使测试不通过,也只需回滚这一台实例,用户无感知。

    2. 灰度放量

      通过金丝雀测试后,可以逐渐放量到新的版本上。例如,根据userId或者ip放5%的流量到其中一台灰度实例上,观察一段时间没异常,可调整放入20%的流量,如果一台实例扛不住,可再发一台或多台实例。将发布产生的风险保持在可控范围内。

    3. 切断实例流量

      当线上出现问题,可将某台实例的流量切断,保留现场,设置指定的请求进入实例,在线调试并且不影响其它用户。

    4. 数据透传

      借助灰度追踪的能力,在网关处记录用户请求的最初的数据,可以将之透传到请求完整的调用链中。

    5. 借助“破窗”能力,实例蓝绿发布

      首次上灰度时,会存在两种环境,一种是已经依赖了灰度客户端的环境,另一种是正常运行的当前环境。假如微服务的负载均衡是由ribbon实现,那么当前环境会请求路由到实例状态为UP的实例上,而依赖了灰度客户端的环境,则可以通过"破窗"能力,跟灰度路由结合,可以将匹配灰度策略的请求路由到实例状态为STARTING的实例上,不匹配灰度策略的请求路由到实例状态为UP的实例上。

    设计思想

    在微服务架构中,接口的调用通常是服务消费方按照某种负载均衡策略去选择服务实例;但这无法满足线上更特殊化的一些路由逻辑,比如根据一次请求携带的请求头中的信息路由到某一个服务实例上。Spring Cloud Gray正是为此而创建。
    在Spring Cloud Gray中定义了几个角色灰度客户端(gray-client)、灰度管控端(gray-server)、注册中心。

    注册中心负责服务的注册和发现。

    灰度客户端灰度的客户端是指依赖了spring-cloud-gray-client的服务,一般是指服务消费方。

    灰度管控端负责灰度信息的管理、持久化等维护工作。
    灰度客户端会从灰度管控端拉取一份灰度信息的清单,并在内存中维护这份清单信息,清单中包含服务,服务实例,灰度策略,灰度追踪字段等。
    当请求达到网关时,网关就会在灰度追踪中将需要透传的信息记录下来,并将传递给转发的服务实例,后面的接口调用也会按照同样的逻辑将追踪信息透传下去,从而保证所有一个请求在微服务调用链中的灰度路由。
    如下图所示:

    管控端的功能

    1. 用户管理

      可添加用户,禁用用户,重置密码等。

    2. 服务列表

    3. 权限控制

      灰度的权限控制是以服务为对象的,拥有服务的权限,就可以操作服务的所有灰度信息。在服务的权限控制中,分为两种角色,owner和管理者,owner拥有最大的权限,管理者除了不能删除owner的权限,其它权限同owner一样。 owner listauthrity list

    4. 灰度实例管理

      列出服务的灰度实例列表

    5. 在线实例列表

      列出指定服务在注册中心注册的实例,点击【Add】按钮,可快速添加为实例实例

    6. 编辑灰度策略

      从实例列表点击【策略】按钮进入灰度策略列表,可在策略列表中添加灰度策略和灰度决策。

      实例的灰度策略,包含可多个灰度决策。
 策略是从灰度实例列表进入。一个实例可以有多个灰度策略,策略与策略之间是"或"的关系。就是说,一个请求只要 满足实例的任意一个灰度策略,这个请求被路由到该实例上。

      决策是灰度中进行比对的最小项。它定义一种规则,对请求进行比对,返回 true/false。当请求调用时,灰度调用端可以根据灰度实例的灰度决策,进行对比,以判断灰度实例是否可以受理该请求。多个决策是"与"的关系。 灰度策略灰度决策

    7. 编辑灰度追踪

      从服务列表点击【追踪】按钮进入

    8. 改变实例状态

      可在实例列表中,通过【实例状态】按钮修改实例状态。提前是实例得依赖了灰度客户端的jar包,并且uri没有设置前缀(server.servlet.context-path)

    9. 操作审记

      所有的POST,PUT,DELETE操作都会被记录下来,可能通过操作记录查询,用于事后审计。

      查询维度包括:

    • StartTime - EndTime 记录时间

    • Operator Id 操作人Id

    • ApiRes Code 接口返回的ApiRes Code

    • operate State 操作的结果

    • Request Hander spring mvc 的接口(Controller/Handler)的类名和方法

    • URI 接口的uri

    • IP http请求(操作人)的ip

    工程模块

    功能模块

    模块描述
    spring-cloud-gray-utils工具包
    spring-cloud-gray-core灰度数据模型/Java Bean定义,client端和server端通用
    spring-cloud-gray-client灰度客户端的核心代码,属于灰度客户端的内核
    spring-cloud-gray-client-netflix灰度客户端与spring cloud netflix集成的代码,与之相关的插件都依赖这个模块
    spring-cloud-gray-plugin-webmvc支撑灰度客户端在spring mvc运行的插件
    spring-cloud-gray-plugin-webflux支撑灰度客户端在spring webfulx运行的插件(B版及以上)
    spring-cloud-gray-plugin-eureka灰度客户端与注册中心eureka集成的插件
    spring-cloud-gray-plugin-feign灰度客户端与openFiegn集成的插件
    spring-cloud-gray-plugin-zuul灰度客户端与zuul 1.0集成的插件
    spring-cloud-gray-plugin-gateway灰度客户端与spring cloud gateway集成的插件(B版及以上)
    spring-cloud-gray-plugin-event-stream灰度客户端与spring cloud stream(rabbitmq)集成的插件
    spring-cloud-gray-plugin-ribbon-nacos-discovery灰度客户端支持ribbon与注册中心nacos集成的插件
    spring-cloud-gray-server灰度管控端的核心代码
    spring-cloud-gray-server-plugin-eureka灰度管控端与注册中心eureka集成的插件
    spring-cloud-gray-server-plugin-event-stream灰度管控端与spring cloud stream(rabbitmq)集成的插件
    spring-cloud-gray-server-plugin-nacos-discovery灰度管控端与注册中心nacos集成的插件
    spring-cloud-starter-gray-client灰度客户端starter
    spring-cloud-starter-gray-server灰度管控端starter
    spring-cloud-starter-gray-eureka-servereureka server的灰度插件
    spring-cloud-gray-webui灰度管控端的web界面,vue编写

    示例模块

    模块描述
    spring-cloud-gray-eureka-sampleeureka server/注册中心
    spring-cloud-gray-server-sample灰度管控端示例,界面是spring-cloud-gray-webui模块
    spring-cloud-gray-service-a-sample服务提供方示例
    spring-cloud-gray-service-a1-sample服务提供方示例
    spring-cloud-gray-ervice-b-sample服务消费方示例
    spring-cloud-gray-zuul-samplezuul网关示例
    spring-cloud-gray-gateway-samplespring-cloud-gateway网关示例(B版及以上)

    灰度决策

    灰度决策是灰度路由的关键,灰度决策由工厂类创建,工厂类的抽象接口是cn.springcloud.gray.decision.factory.GrayDecisionFactory。
    目前已有的灰度决策有:

    名称工厂类描述
    HttpHeaderHttpHeaderGrayDecisionFactory根据http请求头的字段进行判断
    HttpMethodHttpMethodGrayDecisionFactory根据http请求方法的字段进行判断
    HttpParameterHttpParameterGrayDecisionFactory根据http url参数进行判断
    HttpTrackHeaderHttpTrackHeaderGrayDecisionFactory根据灰度追踪记录的http请求头的字段进行判断
    HttpTrackParameterHttpTrackParameterGrayDecisionFactory根据灰度追踪记录的http url参数进行判断
    TraceIpGrayTraceIpGrayDecisionFactory根据灰度追踪记录的请求ip进行判断
    TrackAttributeTrackAttributeGrayDecisionFactory根据灰度追踪记录的属性值进行判断
    FlowRateGrayFlowRateGrayDecisionFactory按百分比放量进行判断

    自定义灰度决策实现

    如果上面这些决策还不能满足需求,那么可以扩展 cn.springcloud.gray.decision.factory.GrayDecisionFactory,实现自定义的逻辑,发布到spring 容器中即可。如:

    importcn.springcloud.gray.decision.GrayDecision;importcn.springcloud.gray.decision.factory.AbstractGrayDecisionFactory;importcn.springcloud.gray.request.GrayHttpTrackInfo;importlombok.Getter;importlombok.Setter;importorg.apache.commons.lang3.StringUtils;importorg.springframework.stereotype.Component;@ComponentpublicclassVersionGrayDecisionFactoryextendsAbstractGrayDecisionFactory<VersionGrayDecisionFactory.Config> {publicVersionGrayDecisionFactory() {super(VersionGrayDecisionFactory.Config.class);
        }@OverridepublicGrayDecisionapply(ConfigconfigBean) {returnargs->{GrayHttpTrackInfograyRequest=(GrayHttpTrackInfo) args.getGrayRequest().getGrayTrackInfo();intversion=StringUtils.defaultIfNull(grayRequest.getAttribute(USER_ID_PARAM_NAME),"0");if(StringUtils.equal(configBean.getCompareMode(),"LT")){returnconfigBean.getVersion()>version;
                }elseif(StringUtils.equal(configBean.getCompareMode(),"GT")){returnconfigBean.getVersion()<version;
                }else{returnconfigBean.getVersion()==version;
                }
            };
        }@Setter@GetterpublicstaticclassConfig{privateStringcompareMode;privateintvarsion;
        }
    }

    灰度追踪

    灰度追踪记录的逻辑是由cn.springcloud.gray.request.GrayInfoTracker的实现类实现。
    目前已有的灰度追踪有:

    名称实现类描述
    HttpReceiveHttpReceiveGrayInfoTracker接收调用端传递过来的灰追踪信息
    HttpHeaderHttpHeaderGrayInfoTracker获取http请求的header并记录到灰度追踪的Header中
    HttpIPHttpIPGrayInfoTracker获取http请求的ip并记录到灰度追踪中
    HttpMethodHttpMethodGrayInfoTracker获取http请求的请求方法并记录到灰度追踪中
    HttpParameterHttpParameterGrayInfoTracker获取http请求的url参数并记录到灰度追踪的parameter中
    HttpURIHttpURIGrayInfoTracker获取http请求的URI并记录到灰度追踪中

    自定义灰度追踪实现

    如果上面这些决策还不能满足需求,那么可以扩展 cn.springcloud.gray.request.GrayInfoTracker,实现自定义的逻辑,发布到spring 容器中即可。如:

    importcn.springcloud.gray.request.GrayHttpTrackInfo;importcn.springcloud.gray.request.TrackArgs;importcn.springcloud.gray.web.tracker.HttpGrayInfoTracker;importlombok.extern.slf4j.Slf4j;importorg.springframework.security.core.Authentication;importorg.springframework.security.core.context.SecurityContext;importorg.springframework.security.core.context.SecurityContextHolder;importorg.springframework.security.core.userdetails.UserDetails;importorg.springframework.stereotype.Component;importjavax.servlet.http.HttpServletRequest;@Slf4j@ComponentpublicclassUserIdGrayInfoTrackerimplementsHttpGrayInfoTracker{@Overridepublicvoidcall(TrackArgs<GrayHttpTrackInfo,HttpServletRequest>args) {SecurityContextsecurityContext=SecurityContextHolder.getContext();Authenticationauthentication=securityContext.getAuthentication();StringuserId=null;if(authentication.getPrincipal()instanceofUserDetails) {UserDetailsspringSecurityUser=(UserDetails) authentication.getPrincipal();
                userId=springSecurityUser.getUsername();
            }elseif(authentication.getPrincipal()instanceofString) {
                userId=(String) authentication.getPrincipal();
            }
            args.getTrackInfo().setAttribute("userId", userId);
        }
    }

    项目扩展

    项目已经实现了灰度的内核,如果要与其它的注册中心或者负载均衡中间件集成,只需实现相应的plugin即可,spring cloud gray已经提供了eureka、ribbon、feign、zuul以及spring cloud gateway和spring cloud stream的plugin,添加相应的plugin依赖即可。

    Spring Cloud Gateway(限流) | Wind Mt

    $
    0
    0

    game of thrones

    绝境长城(冰与火之歌)

    在高并发的应用中, 限流是一个绕不开的话题。限流可以保障我们的 API 服务对所有用户的可用性,也可以防止网络攻击。

    一般开发高并发系统常见的限流有:限制总并发数(比如数据库连接池、线程池)、限制瞬时并发数(如 nginx 的 limit_conn 模块,用来限制瞬时并发连接数)、限制时间窗口内的平均速率(如 Guava 的 RateLimiter、nginx 的 limit_req 模块,限制每秒的平均速率);其他还有如限制远程接口调用速率、限制 MQ 的消费速率。另外还可以根据网络连接数、网络流量、CPU 或内存负载等来限流。

    本文详细探讨在 Spring Cloud Gateway 中如何实现限流。

    限流算法

    做限流 (Rate Limiting/Throttling) 的时候,除了简单的控制并发,如果要准确的控制 TPS,简单的做法是维护一个单位时间内的 Counter,如判断单位时间已经过去,则将 Counter 重置零。此做法被认为没有很好的处理单位时间的边界,比如在前一秒的最后一毫秒里和下一秒的第一毫秒都触发了最大的请求数,也就是在两毫秒内发生了两倍的 TPS。

    常用的更平滑的限流算法有两种:漏桶算法和令牌桶算法。很多传统的服务提供商如华为中兴都有类似的专利,参考 采用令牌漏桶进行报文限流的方法

    漏桶算法

    漏桶( Leaky Bucket)算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率。

    Leaky Bucket

    可见这里有两个变量,一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(rate)。因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流突发(burst)到端口速率。因此,漏桶算法对于存在突发特性的流量来说缺乏效率。

    令牌桶算法

    令牌桶算法(Token Bucket)和 Leaky Bucket 效果一样但方向相反的算法,更加容易理解。随着时间流逝,系统会按恒定 1/QPS 时间间隔(如果 QPS=100,则间隔是 10ms)往桶里加入 Token(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了就不再加了。新请求来临时,会各自拿走一个 Token,如果没有 Token 可拿了就阻塞或者拒绝服务。

    Token Bucket

    令牌桶的另外一个好处是可以方便的改变速度。一旦需要提高速率,则按需提高放入桶中的令牌的速率。一般会定时(比如 100 毫秒)往桶中增加一定数量的令牌,有些变种算法则实时的计算应该增加的令牌的数量。

    Guava 中的 RateLimiter 采用了令牌桶的算法,设计思路参见 How is the RateLimiter designed, and why?,详细的算法实现参见 源码

    Leakly Bucket vs Token Bucket

    对比项Leakly bucketToken bucketToken bucket 的备注
    依赖 token
    立即执行有足够的 token 才能执行
    堆积 token
    速率恒定可以大于设定的 QPS

    限流实现

    在 Gateway 上实现限流是个不错的选择,只需要编写一个过滤器就可以了。有了前边过滤器的基础,写起来很轻松。(如果你对 Spring Cloud Gateway 的过滤器还不了解,请先看 这里

    我们这里采用令牌桶算法,Google Guava 的 RateLimiterBucket4jRateLimitJ都是一些基于此算法的实现,只是他们支持的 back-ends(JCache、Hazelcast、Redis 等)不同罢了,你可以根据自己的技术栈选择相应的实现。

    这里我们使用 Bucket4j,引入它的依赖坐标,为了方便顺便引入 Lombok

    1            
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <dependency>            
    <groupId>com.github.vladimir-bukhtoyarov</groupId>
    <artifactId>bucket4j-core</artifactId>
    <version>4.0.0</version>
    </dependency>

    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.16.20</version>
    <scope>provided</scope>
    </dependency>

    我们来实现具体的过滤器

    1            
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    @CommonsLog            
    @Builder
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    publicclassRateLimitByIpGatewayFilterimplementsGatewayFilter,Ordered{

    intcapacity;
    intrefillTokens;
    Duration refillDuration;

    privatestaticfinalMap<String,Bucket> CACHE =newConcurrentHashMap<>();

    privateBucketcreateNewBucket(){
    Refill refill = Refill.of(refillTokens,refillDuration);
    Bandwidth limit = Bandwidth.classic(capacity,refill);
    returnBucket4j.builder().addLimit(limit).build();
    }

    @Override
    publicMono<Void>filter(ServerWebExchange exchange,GatewayFilterChain chain){
    // if (!enableRateLimit){
    // return chain.filter(exchange);
    // }
    String ip = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
    Bucket bucket = CACHE.computeIfAbsent(ip,k -> createNewBucket());

    log.debug("IP: "+ ip +",TokenBucket Available Tokens: "+ bucket.getAvailableTokens());
    if(bucket.tryConsume(1)) {
    returnchain.filter(exchange);
    }else{
    exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
    returnexchange.getResponse().setComplete();
    }
    }

    @Override
    publicintgetOrder(){
    return-1000;
    }

    }

    通过对令牌桶算法的了解,我们知道需要定义三个变量:

    • capacity:桶的最大容量,即能装载 Token 的最大数量
    • refillTokens:每次 Token 补充量
    • refillDuration:补充 Token 的时间间隔

    在这个实现中,我们使用了 IP 来进行限制,当达到最大流量就返回 429错误。这里我们简单使用一个 Map 来存储 bucket,所以也决定了它只能单点使用,如果是分布式的话,可以采用 Hazelcast 或 Redis 等解决方案。

    在 Route 中我们添加这个过滤器,这里指定了 bucket 的容量为 10 且每一秒会补充 1 个 Token。

    1            
    2
    3
    4
    5
    6
    7
    .route(r -> r.path("/throttle/customer/**")            
    .filters(f -> f.stripPrefix(2)
    .filter(newRateLimitByIpGatewayFilter(10,1,Duration.ofSeconds(1))))
    .uri("lb://CONSUMER")
    .order(0)
    .id("throttle_customer_service")
    )

    启动服务并多次快速刷新改接口,就会看到 Tokens 的数量在不断减小,等一会又会增加上来

    1            
    2
    3
    4
    2018-05-09 15:42:08.601 DEBUG 96278 --- [ctor-http-nio-2] com.windmt.filter.RateLimitByIpGatewayFilter  : IP: 0:0:0:0:0:0:0:1,TokenBucket Available Tokens: 2            
    2018-05-09 15:42:08.958 DEBUG 96278 --- [ctor-http-nio-2] com.windmt.filter.RateLimitByIpGatewayFilter : IP: 0:0:0:0:0:0:0:1,TokenBucket Available Tokens: 1
    2018-05-09 15:42:09.039 DEBUG 96278 --- [ctor-http-nio-2] com.windmt.filter.RateLimitByIpGatewayFilter : IP: 0:0:0:0:0:0:0:1,TokenBucket Available Tokens: 0
    2018-05-09 15:42:10.201 DEBUG 96278 --- [ctor-http-nio-2] com.windmt.filter.RateLimitByIpGatewayFilter : IP: 0:0:0:0:0:0:0:1,TokenBucket Available Tokens: 1

    RequestRateLimiter

    刚刚我们通过过滤器实现了限流的功能,你可能在想为什么不直接创建一个过滤器工厂呢,那样多方便。这是因为 Spring Cloud Gateway 已经内置了一个 RequestRateLimiterGatewayFilterFactory,我们可以直接使用(这里有坑,后边详说)。

    目前 RequestRateLimiterGatewayFilterFactory的实现依赖于 Redis,所以我们还要引入 spring-boot-starter-data-redis-reactive

    1            
    2
    3
    4
    <dependency>            
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
    </dependency>

    因为这里有坑,所以把 application.yml 的配置再全部贴一遍,新增的部分我已经用 # ---标出来了

    1            
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    spring:            
    application:
    name:cloud-gateway
    cloud:
    gateway:
    discovery:
    locator:
    enabled:true
    routes:
    -id:service_customer
    uri:lb://CONSUMER
    order:0
    predicates:
    -Path=/customer/**
    filters:
    -StripPrefix=1
    # -------
    -name:RequestRateLimiter
    args:
    key-resolver:'#{@remoteAddrKeyResolver}'
    redis-rate-limiter.replenishRate:1
    redis-rate-limiter.burstCapacity:5
    # -------
    -AddResponseHeader=X-Response-Default-Foo,Default-Bar
    default-filters:
    -Elapsed=true
    # -------
    redis:
    host:localhost
    port:6379
    database:0
    # -------
    server:
    port:10000
    eureka:
    client:
    service-url:
    defaultZone:http://localhost:7000/eureka/
    logging:
    level:
    org.springframework.cloud.gateway:debug
    com.windmt.filter:debug

    默认情况下,是基于 令牌桶算法实现的限流,有个三个参数需要配置:

    • burstCapacity,令牌桶容量。
    • replenishRate,令牌桶每秒填充平均速率。
    • key-resolver,用于限流的键的解析器的 Bean 对象名字(有些绕,看代码吧)。它使用 SpEL 表达式根据 #{@beanName}从 Spring 容器中获取 Bean 对象。默认情况下,使用 PrincipalNameKeyResolver,以请求认证的 java.security.Principal作为限流键。

    关于 filters的那段配置格式,参考 这里

    我们实现一个使用请求 IP 作为限流键的 KeyResolver

    1            
    2
    3
    4
    5
    6
    7
    8
    9
    publicclassRemoteAddrKeyResolverimplementsKeyResolver{            
    publicstaticfinalString BEAN_NAME ="remoteAddrKeyResolver";

    @Override
    publicMono<String>resolve(ServerWebExchange exchange){
    returnMono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
    }

    }

    配置 RemoteAddrKeyResolverBean 对象

    1            
    2
    3
    4
    @Bean(name = RemoteAddrKeyResolver.BEAN_NAME)            
    publicRemoteAddrKeyResolverremoteAddrKeyResolver(){
    returnnewRemoteAddrKeyResolver();
    }

    以上就是代码部分,我们还差一个 Redis,我就本地用 docker 来快速启动了

    1            
    docker run --name redis -p 6379:6379 -d redis            

    万事俱备,只欠测试了。以上的代码的和配置都是 OK 的,可以自行测试。下面来说一下这里边的坑。

    遇到的坑

    配置不生效

    参考这个 issue

    No Configuration found for route

    这个异常信息如下:

    1            
    2
    java.lang.IllegalArgumentException: No Configuration found for route service_customer            
    at org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter.isAllowed(RedisRateLimiter.java:93) ~[spring-cloud-gateway-core-2.0.0.RC1.jar:2.0.0.RC1]

    出现在将 RequestRateLimiter 配置为 defaultFilters 的情况下,比如像这样

    1            
    2
    3
    4
    5
    6
    default-filters:            
    -name:RequestRateLimiter
    args:
    key-resolver:'#{@remoteAddrKeyResolver}'
    redis-rate-limiter.replenishRate:1
    redis-rate-limiter.burstCapacity:5

    这时候就会导致这个异常。我通过分析源码,发现了一些端倪,感觉像是一个 bug,已经提交了 issue

    我们从异常入手来看, RedisRateLimiter#isAllowed这个方法要获取 routeId 对应的 routerConfig,如果获取不到就抛出刚才我们看到的那个异常。

    1            
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    publicMono<Response>isAllowed(String routeId,String id){            
    if(!this.initialized.get()) {
    thrownewIllegalStateException("RedisRateLimiter is not initialized");
    }
    // 只为 defaultFilters 配置 RequestRateLimiter 的时候
    // config map 里边的 key 只有 "defaultFilters"
    // 但是我们实际请求的 routeId 为 "customer_service"
    Config routeConfig = getConfig().get(routeId);

    if(routeConfig ==null) {
    if(defaultConfig ==null) {
    thrownewIllegalArgumentException("No Configuration found for route "+ routeId);
    }
    routeConfig = defaultConfig;
    }

    // 省略若干代码...
    }

    既然这里要 get,那必然有个地方要 put。put 的相关代码在 AbstractRateLimiter#onApplicationEvent这个方法。

    1            
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    @Override            
    publicvoidonApplicationEvent(FilterArgsEvent event){
    Map<String,Object> args = event.getArgs();

    // hasRelevantKey 检查 args 是否包含 configurationPropertyName
    // 只有 defaultFilters 包含
    if(args.isEmpty() || !hasRelevantKey(args)) {
    return;
    }

    String routeId = event.getRouteId();
    C routeConfig = newConfig();
    ConfigurationUtils.bind(routeConfig,args,
    configurationPropertyName,configurationPropertyName,validator);
    getConfig().put(routeId,routeConfig);
    }

    privatebooleanhasRelevantKey(Map<String,Object> args){
    returnargs.keySet().stream()
    .anyMatch(key -> key.startsWith(configurationPropertyName +"."));
    }

    上边的 args 里是是配置参数的键值对,比如我们之前自定义的过滤器工厂 Elapsed,有个参数 withParams,这里就是 withParams=true。关键代码在第 7 行, hasRelevantKey方法用于检测 args 里边是否包含 configurationPropertyName.,具体到本例就是是否包含 redis-rate-limiter.。悲剧就发生在这里,因为我们只为 defaultFilters 配置了相关 args,注定其他的 route 到这里就直接 return 了。

    现在不清楚这是 bug 还是设计者有意为之,等答复吧。

    基于系统负载的动态限流

    在实际工作中,我们可能还需要根据网络连接数、网络流量、CPU 或内存负载等来进行动态限流。在这里我们以 CPU 为栗子。

    我们需要借助 Spring Boot Actuator 提供的 Metrics 能力进行实现基于 CPU 的限流——当 CPU 使用率高于某个阈值就开启限流,否则不开启限流。

    我们在项目中引入 Actuator 的依赖坐标

    1            
    2
    3
    4
    <dependency>            
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

    因为 Spring Boot 2.x 之后,Actuator 被重新设计了,和 1.x 的区别还是挺大的(参考 这里)。我们先在配置中设置 management.endpoints.web.exposure.include=*来观察一下新的 Metrics 的能力

    http://localhost:10000/actuator/metrics

    1            
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    {            
    "names": [
    "jvm.buffer.memory.used",
    "jvm.memory.used",
    "jvm.buffer.count",
    "jvm.gc.memory.allocated",
    "logback.events",
    "process.uptime",
    "jvm.memory.committed",
    "system.load.average.1m",
    "jvm.gc.pause",
    "jvm.gc.max.data.size",
    "jvm.buffer.total.capacity",
    "jvm.memory.max",
    "system.cpu.count",
    "system.cpu.usage",
    "process.files.max",
    "jvm.threads.daemon",
    "http.server.requests",
    "jvm.threads.live",
    "process.start.time",
    "jvm.classes.loaded",
    "jvm.classes.unloaded",
    "jvm.threads.peak",
    "jvm.gc.live.data.size",
    "jvm.gc.memory.promoted",
    "process.files.open",
    "process.cpu.usage"
    ]
    }

    我们可以利用里边的系统 CPU 使用率 system.cpu.usage

    http://localhost:10000/actuator/metrics/system.cpu.usage

    1            
    2
    3
    4
    5
    6
    7
    8
    9
    10
    {            
    "name":"system.cpu.usage",
    "measurements": [
    {
    "statistic":"VALUE",
    "value":0.5189003436426117
    }
    ],
    "availableTags": []
    }

    最近一分钟内的平均负载 system.load.average.1m也是一样的

    http://localhost:10000/actuator/metrics/system.load.average.1m

    1            
    2
    3
    4
    5
    6
    7
    8
    9
    10
    {            
    "name":"system.load.average.1m",
    "measurements": [
    {
    "statistic":"VALUE",
    "value":5.33203125
    }
    ],
    "availableTags": []
    }

    知道了 Metrics 提供的指标,我们就来看在代码里具体怎么实现吧。Actuator 2.x 里边已经没有了之前 1.x 里边提供的 SystemPublicMetrics,但是经过阅读源码可以发现 MetricsEndpoint这个类可以提供类似的功能。就用它来撸代码吧

    1            
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    @CommonsLog            
    @Component
    publicclassRateLimitByCpuGatewayFilterimplementsGatewayFilter,Ordered{

    @Autowired
    privateMetricsEndpoint metricsEndpoint;

    privatestaticfinalString METRIC_NAME ="system.cpu.usage";
    privatestaticfinaldoubleMAX_USAGE =0.50D;

    @Override
    publicMono<Void>filter(ServerWebExchange exchange, GatewayFilterChain chain){
    // if (!enableRateLimit){
    // return chain.filter(exchange);
    // }
    Double systemCpuUsage = metricsEndpoint.metric(METRIC_NAME,null)
    .getMeasurements()
    .stream()
    .filter(Objects::nonNull)
    .findFirst()
    .map(MetricsEndpoint.Sample::getValue)
    .filter(Double::isFinite)
    .orElse(0.0D);

    booleanok = systemCpuUsage < MAX_USAGE;

    log.debug("system.cpu.usage: "+ systemCpuUsage +" ok: "+ ok);

    if(!ok) {
    exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
    returnexchange.getResponse().setComplete();
    }else{
    returnchain.filter(exchange);
    }
    }

    @Override
    publicintgetOrder(){
    return0;
    }

    }

    配置 Route

    1            
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Autowired            
    privateRateLimitByCpuGatewayFilter rateLimitByCpuGatewayFilter;

    @Bean
    publicRouteLocatorcustomerRouteLocator(RouteLocatorBuilder builder){
    // @formatter:off
    returnbuilder.routes()
    .route(r -> r.path("/throttle/customer/**")
    .filters(f -> f.stripPrefix(2)
    .filter(rateLimitByCpuGatewayFilter))
    .uri("lb://CONSUMER")
    .order(0)
    .id("throttle_customer_service")
    )
    .build();
    // @formatter:on
    }

    至于效果嘛,自己试试吧。因为 CPU 的使用率一般波动较大,测试效果还是挺明显的,实际使用就得慎重了。

    示例代码可以从 Github 获取: https://github.com/zhaoyibo/spring-cloud-study

    改进与提升

    实际项目中,除以上实现的限流方式,还可能会:一、在上文的基础上,增加配置项,控制每个路由的限流指标,并实现动态刷新,从而实现更加灵活的管理。二、实现不同维度的限流,例如:

    • 对请求的目标 URL 进行限流(例如:某个 URL 每分钟只允许调用多少次)
    • 对客户端的访问 IP 进行限流(例如:某个 IP 每分钟只允许请求多少次)
    • 对某些特定用户或者用户组进行限流(例如:非 VIP 用户限制每分钟只允许调用 100 次某个 API 等)
    • 多维度混合的限流。此时,就需要实现一些限流规则的编排机制(与、或、非等关系)

    参考

    Token bucket
    RequestRateLimiter GatewayFilter Factory
    Scaling your API with rate limiters
    Scaling your API with rate limiters 译文
    Spring Boot Actuator Web API Documentation
    https://github.com/nereuschen/blog/issues/37
    http://www.itmuch.com/spring-cloud-sum/spring-cloud-ratelimit


    Spring Cloud 不停机发布服务(0-downtime Blue/Green deployments) | 帆的博客

    $
    0
    0

    背景

    项目初期由于BUG和需求改动可能都会比较多,我们会很频繁的发布我们的应用。但是如果不进行处理,在升级的过程中会导致用户服务中断。

    通常我们需要发布的内容如下:

    1. 某一个服务BUG紧急修复。
    2. 某一个服务新的需求上线。

    实际上针对这两种情况,在传统的应用中我们是很容易做到不停机升级的。例如nginx负载均衡2台tomcat实例,在升级的时候切断其中一台访问,升级完成以后切换流量,再升级另外一台。但是我这里用的是Spring Cloud,所有的实例状态都维护在Eureka中,Eureka本身也提供了很多保护机制,所以你的服务在down掉的时候,不会立马从服务列表中剔除掉。具体的配置项可以周立老师一篇文章里查看: 如何解决Eureka Server不踢出已关停的节点的问题

    所以如果我们想要做到不停机去升级/发布一个服务,需要我们从Spring Cloud架构本身上着手去进行一些改造。我们需要去了解Eureka的使用方式,Spring Retry的使用,Spring Cloud的负载均衡规则等等,最终达到这个目的。

    思路

    如果一个不了解Spring Cloud的人来做这种不停机发布,比如运维部门的同事。他会将某个需要升级的实例新版本启动起来,然后将老版本的进程杀掉。但是因为Spring Cloud的特性,被干掉的实例并没有被踢出服务列表,客户端仍然会访问到一个不存在的实例,直接返回500错误。可能需要等1~2分钟以后才能恢复正常。

    我们知道这个是因为Eureka的机制问题,但是它注定不可能做成实时感知上下线的。Eureka是通过定期扫描去下线已经down掉的服务,不过他的默认时间是60秒,我们可以优化这个配置,让它能比较快的感知到服务已经下线。

    关于Eureka的常见问题

    中小规模生产环境参考配置:

    Eureka Server

    1            
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    eureka:            
    server:
    enable-self-preservation:false# 中小规模下,自我保护模式坑比好处多,所以关闭它
    eviction-interval-timer-in-ms:5000# 续期时间,即扫描失效服务的间隔时间(缺省为60*1000ms)从服务列表中剔除
    use-read-only-response-cache:false# 禁用readOnlyCacheMap
    instance:
    lease-renewal-interval-in-seconds:5# 心跳时间,即服务续约间隔时间(缺省为30s)
    lease-expiration-duration-in-seconds:10# 没有心跳的淘汰时间,10秒,即服务续约到期时间(缺省为90s)
    client:
    service-url:
    defaultZone:${defaultZone:http://peer2:8760/eureka/}

    Eureka Client

    1            
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    eureka:            
    instance:
    lease-renewal-interval-in-seconds:5# 心跳时间,即服务续约间隔时间(缺省为30s)
    lease-expiration-duration-in-seconds:10# 没有心跳的淘汰时间,10秒,即服务续约到期时间(缺省为90s)
    client:
    # 向注册中心注册
    fetch-registry:true
    # 服务清单的缓存更新时间,默认30秒一次
    registry-fetch-interval-seconds:5
    service-url:
    defaultZone:${defaultZone:http://${DISCOVERY_URL:discovery}:8761/eureka/}

    通过优化Eureka配置,服务在启动后能够较快的被使用上,Eureka也能较快的感知到服务以及下线并踢出服务列表。

    巧用Spring Retry重试机制

    我在搜寻解决方案的时候,也看到了Github上讨论的一个issue: Best practices for using Eureka for 0-downtime Blue/Green deployments #1290.

    这里面讨论了利用重试机制去实现不停机发布的一种方式。前面的Eureka配置已经缩短了服务上线和服务下线的时间,但是这中间仍然一段延迟,可能还是会有请求随机访问一个不存在的服务实例上。

    重试机制的原理就是利用Spring Cloud提供的重试机制在请求访问出现错误的时候自动重试当前实例或者其他实例,而不是直接返回错误。

    主要配置如下:

    1            
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    ribbon:            
    # ribbon缓存时间
    ServerListRefreshInterval:2000
    ReadTimeout:30000
    ConnectTimeout:30000
    # 是否所有操作都重试
    # OkToRetryOnAllOperations: true
    # 重试负载均衡其他的实例最大重试次数,不包括首次server
    MaxAutoRetriesNextServer:0
    # 同一台实例最大重试次数,不包括首次调用
    MaxAutoRetries:0
    zuul:
    retryable:true

    但是这里要注意一点, OkToRetryOnAllOperations如果设置为true,那么ribbon超时时间最好设置长一点,否则post等请求如果超时会被提交多次,还要注意hystrix的超时时间要大于ribbion的超时时间,否则hystrix会先超时。

    1            
    2
    3
    4
    5
    6
    7
    hystrix:            
    command:
    default:
    execution:
    isolation:
    thread:
    timeoutInMilliseconds:40000

    在不同的版本中,Spring Cloud的重试机制是比较混乱的,周立老师对重试机制的详细解释: http://www.itmuch.com/spring-cloud-sum/spring-cloud-retry/

    Feign本身也具备重试能力,在早期的Spring Cloud中,Feign使用的是 feign.Retryer.Default#Default(),重试5次。但Feign整合了Ribbon,Ribbon也有重试的能力,此时,就可能会导致行为的混乱。

    Spring Cloud意识到了此问题,因此做了改进,将Feign的重试改为 feign.Retryer#NEVER_RETRY,如需使用Feign的重试,只需使用Ribbon的重试配置即可。因此,对于Camden以及以后的版本,Feign的重试可使用如下属性进行配置:

    1            
    2
    3
    4
    ribbon:            
    MaxAutoRetries:1
    MaxAutoRetriesNextServer:2
    OkToRetryOnAllOperations:false

    相关Issue可参考: https://github.com/spring-cloud/spring-cloud-netflix/issues/467

    结合之前对Eureka配置的优化,我们就可以愉快的进行测试了,开启2个服务访问几次,可以发现随机访问。然后干掉一个服务,再次访问,依然没有问题,不会出现500等情况。Feign自动为我们选择了另外可用的服务发送了重试请求。

    灰度发布方案

    还有一种特别的需求,我们除了想做到不停机发布,可能还需要做到某些用户测试新版本代码,实现降级、限流、滚动、灰度、AB、金丝雀等操作。我在Github上发现了一个开源的代码在一定程度上提供了很好的思路去做这个事情。地址: https://github.com/JeromeLiuLly/springcloud-gray

    看这个实现方式可以看出来,他的方案是基于 spring cloud 实践-降级、限流、滚动、灰度、AB、金丝雀等等等等的方案做的。

    因Spring Cloud都是客户端负载均衡,会从Eureka读取服务列表,然后通过一定的负载均衡规则来选择请求的服务器。这个方案就是重写了Ribbon负载均衡的策略,将一些自定义信息放入了Eureka的metdata-map中,在路由的时候根据这些信息来选择服务。我这里不再多说,大家可以自行去查看他们的文章和代码。

    这个方案灵活性非常大,你可以根据自定义的信息来构建任何你想做的策略,去实现AB Test等等功能,甚至我在开发环境中也能使用。举个例子,因为我们的服务太多了,如果在本机开发的时候,关联的服务较多,要启动比较多的服务才能够进行开发和测试,可能机器会有点吃不消。我基于上述方案让开发的同学们在启动服务的上将本机的IP添加到 metadata-map中,这样我在路由的时候判断客户端请求过来的IP是多少,如果跟实例里的信息匹配,那么所有来自这个IP请求就转发到开发同学启动的那台实例上。

    spring cloud灰度发布快速上下线问题解决 - 简书

    $
    0
    0

    因为目前公司架构全部切换到spring cloud 模式,对于服务灰度方面没有dubbo zk的方便了,所以细细研究总结下留作备份。目前业界有几种流行的发布部署策略,从网上资料可以搜索到,不是这次重点贴出来看看就行了。

    目前部署的几种策略

    蓝绿部署

    蓝绿部署无需停机,并且风险较小。
    (1) 部署版本1的应用(一开始的状态)
    所有外部请求的流量都打到这个版本上。
    (2) 部署版本2的应用
    版本2的代码与版本1不同(新功能、Bug修复等)。
    (3) 将流量从版本1切换到版本2。
    (4) 如版本2测试正常,就删除版本1正在使用的资源(例如实例),从此正式用版本2。

    滚动发布

    滚动发布,一般是取出一个或者多个服务器停止服务,执行更新,并重新将其投入使用。周而复始,直到集群中所有的实例都更新成新版本。
    这种部署方式相对于蓝绿部署,更加节约资源——它不需要运行两个集群、两倍的实例数。我们可以部分部署,例如每次只取出集群的20%进行升级。
    这种方式也有很多缺点,例如:
    (1) 没有一个确定OK的环境。使用蓝绿部署,我们能够清晰地知道老版本是OK的,而使用滚动发布,我们无法确定。
    (2) 修改了现有的环境。
    (3) 如果需要回滚,很困难。举个例子,在某一次发布中,我们需要更新100个实例,每次更新10个实例,每次部署需要5分钟。当滚动发布到第80个实例时,发现了问题,需要回滚。此时,脾气不好的程序猿很可能想掀桌子,因为回滚是一个痛苦,并且漫长的过程。
    (4) 有的时候,我们还可能对系统进行动态伸缩,如果部署期间,系统自动扩容/缩容了,我们还需判断到底哪个节点使用的是哪个代码。尽管有一些自动化的运维工具,但是依然令人心惊胆战。
    并不是说滚动发布不好,滚动发布也有它非常合适的场景。

    灰度发布

    我们来看一下金丝雀部署的步骤:
    (1) 准备好部署各个阶段的工件,包括:构建工件,测试脚本,配置文件和部署清单文件。
    (2) 从负载均衡列表中移除掉“金丝雀”服务器。
    (3) 升级“金丝雀”应用(排掉原有流量并进行部署)。
    (4) 对应用进行自动化测试。
    (5) 将“金丝雀”服务器重新添加到负载均衡列表中(连通性和健康检查)。
    (6) 如果“金丝雀”在线使用测试成功,升级剩余的其他服务器。(否则就回滚)
    灰度发布中,常常按照用户设置路由权重,例如90%的用户维持使用老版本,10%的用户尝鲜新版本。不同版本应用共存,经常与A/B测试一起使用,用于测试选择多种方案。灰度发布比较典型的例子,是阿里云那个“新版本”,点击“进入新版本”,我们就成了金丝雀

    下面进入正题,针对于spring cloud 灰度发布可以分为几个点:
    1、优雅停机
    2、服务快速注册
    3、服务快速订阅
    下面针对于这三个点进行梳理

    一、优雅停机

    你如果使用kill -9 那肯定不算优雅停机了,内部没执行完的线程全部搞死了。所以,优雅停机的关键点:反注册当前服务阻挡前端路由流量、等待应用内部线程执行完毕、反注册内部各种监听器、关闭应用。cloud 可以说为我们提供了两种模式,一种是基于端点的shutdown 接口,另一种就是基于eureka rest api 的模式。

    1、基于shutdown 接口
    其实细细观察这两种模式实现是一样的,先说shutdown 这种模式非常简单。直接执行 http://localhost:8080/shutdown即可。
    下面先说下使用配置:

    1. 加入spring-boot-starter-actuator 模块,反注册基于此模块的shutdown端点接口。
    2. 启用shutdown endpoints.shutdown.enabled=true

    2、基于eureka rest api 进行服务下线配置
    这个其实是eureka 为我们提供好的接口,可以对服务进行各种简单的上下线操作。
    官方地址: https://github.com/Netflix/eureka/wiki/Eureka-REST-operations

    eureka rest api

    这两种方式都可以实现服务快速下线,下面贴下服务shutdown 的源码以供学习参考,此代码为DiscoveryClient 从改类进行各种操作:

    @PreDestroy
        @Override
        public synchronized void shutdown() {
            if (isShutdown.compareAndSet(false, true)) {
                logger.info("Shutting down DiscoveryClient ...");
    
                if (statusChangeListener != null && applicationInfoManager != null) {
                    applicationInfoManager.unregisterStatusChangeListener(statusChangeListener.getId());
                }
    
                cancelScheduledTasks();
    
                // If APPINFO was registered
                if (applicationInfoManager != null && clientConfig.shouldRegisterWithEureka()) {
                    applicationInfoManager.setInstanceStatus(InstanceStatus.DOWN);
                    unregister();
                }
    
                if (eurekaTransport != null) {
                    eurekaTransport.shutdown();
                }
    
                heartbeatStalenessMonitor.shutdown();
                registryStalenessMonitor.shutdown();
    
                logger.info("Completed shut down of DiscoveryClient");
            }
        }

    二、服务快速注册与快速订阅

    针对于这一点可是比较恶心的,因为eureka的注册订阅都是HTTP的,不像dubbo使用了zk可以进行监听,默认的配置也可以使用,但是可能导致一些小问题,比如最主要的就是注册了服务好一会发现不了一直报connection refused.究其原因就是因为cloud 的各种缓存机制的问题。
    先看下问题:

    服务正常注册,最大可能会有120s滞后

    • 30(首次注册 init registe) + 30(readOnlyCacheMap)+30(client fetch interval)+30(ribbon)=120
    • 如果是在Spring Cloud环境下使用这些组件(Eureka, Ribbon),不会有首次注册30秒延迟的问题,服务启动后会马上注册,所以从注册到发现,最多可能是90s。

    服务异常下线:最大可能会有270s滞后

    • 定时清理任务每eureka.server. evictionIntervalTimerInMs(默认60)执行一次清理任务
    • 每次清理任务会把90秒(3个心跳周期,eureka.instance.leaseExpirationDurationInSeconds)没收到心跳的踢除,但是根据官方的说法 ,因为代码实现的bug,这个时间其实是两倍,即180秒,也就是说如果一个客户端因为网络问题或者主机问题异常下线,可能会在180秒后才剔除
    • 读取端,因为readOnlyCacheMap以及客户端缓存的存在,可能会在30(readOnlyCacheMap)+30(client fetch interval)+30(ribbon)=90
    • 所以极端情况最终可能会是180+90=270

    从网上套了一个图给大家看看:


    eureka

    究其原因无非可以分为三个点:
    【服务端缓存】

    • eureka server 缓存,一个ReadWriteMap一个ReadOnlyMap,定期会从ReadWriteMap 把数据同步到ReadOnlyMap 据说这样可以增大吞吐量。
    • 读默认client是从readOnlyCacheMap读取数据,读不到的话再从readWriterCacheMap,还没有再从registry读
    • readOnlyCacheMap 有开关可以关闭useReadOnlyResponseCache

    【客户端缓存】

    • 由eureka.client.registryFetchIntervalSeconds控制,默认30秒。
    • 服务提供者注册服务后会定时心跳,默认如果连续三次心跳失败,同时没有处于保护模式下将进行服务剔除操作,这些参数可在client配置。
    • 因为eureka 会本地缓存配置,所以一般每次定时会从服务端增量拉去。

    【Ribbon缓存】

    • 如果你采用Ribbon来访问服务,那么这里会有个缓存(他的数据来源是本地Eureka Client缓存)

    生产环境配置

    Eureka Server 端配置

    中小规模下,自我保护模式坑比好处多,所以关闭它
    eureka.server.enableSelfPreservation=false
    主动失效检测间隔,配置成5秒
    eureka.server.eviction-interval-timer-in-ms=5000
    禁用readOnlyCacheMap(中小集群可以直接关闭,但在大集群中建议还是开启,使用eureka.server.responseCacheUpdateInvervalMs=3000进行配置)
    eureka.server.use-read-only-response-cache=false

    Eureka 服务提供方配置

    服务过期时间配置,超过这个时间没有接收到心跳EurekaServer就会将这个实例剔除
    注意,EurekaServer一定要设置eureka.server.eviction-interval-timer-in-ms否则这个配置无效,这个配置一般为服务刷新时间配置的三倍,默认90s
    eureka.instance.lease-expiration-duration-in-seconds=15
    服务刷新时间配置,每隔这个时间会主动心跳一次,默认30s
    eureka.instance.lease-renewal-interval-in-seconds=5

    Eureka 服务调用方配置

    定时刷新本地缓存时间
    eureka.client.registry-fetch-interval-seconds=5
    ribbon缓存刷新时间
    ribbon.ServerListRefreshInterval=3000

    至此,cloud简单的灰度发布基本完成,不用担心上下线大面积报错的问题了,但如果需要流量切换,A/B测试等功能要求那就是另说了。
    最后总结下:使用shutdown 接口进行下线操作,然后重启新应用进行上线操作,最长8s服务即可使用,简单易懂。

    关注

    浅谈Kubernetes Ingress控制器的技术选型

    $
    0
    0

    【编者的话】在Kubernetes的实践、部署中,为了解决 Pod 迁移、Node Pod 端口、域名动态分配等问题,需要开发人员选择合适的 Ingress 解决方案。面对市场上众多Ingress产品,开发者该如何分辨它们的优缺点?又该如何结合自身的技术栈选择合适的技术方案呢?在本文中,腾讯云中间件核心研发工程师厉辉将为你介绍如何进行Kubernates Ingress 控制器的技术选型。

    名词解释

    阅读本文需要熟悉以下基本概念:
    • 集群:是指容器运行所需云资源的集合,包含了若干台云服务器、负载均衡器等云资源。
    • 实例(Pod):由相关的一个或多个容器构成一个实例,这些容器共享相同的存储和网络空间。
    • 工作负载(Node):Kubernetes 资源对象,用于管理 Pod 副本的创建、调度以及整个生命周期的自动控制。
    • 服务(Service):由多个相同配置的实例(Pod)和访问这些实例(Pod)的规则组成的微服务。
    • Ingress:Ingress 是用于将外部 HTTP(S)流量路由到服务(Service)的规则集合。


    Kubernetes 访问现状

    1.png

    Kubernetes 的外部访问方式

    在 Kubernetes 中,服务跟 Pod IP 主要供服务在集群内访问使用,对于集群外的应用是不可见的。怎么解决这个问题呢?为了让外部的应用能够访问 Kubernetes 集群中的服务,通常解决办法是 NodePort 和 LoadBalancer。

    这两种方案其实各自都存在一些缺点:
    • NodePort 的缺点是一个端口只能挂载一个 Service,而且为了更高的可用性,需要额外搭建一个负载均衡。
    • LoadBalancer 的缺点则是每个服务都必须要有一个自己的 IP,不论是内网 IP 或者外网 IP。更多情况下,为了保证 LoadBalancer 的能力,一般需要依赖于云服务商。


    在Kubernetes的实践、部署中,为了解决像 Pod 迁移、Node Pod 端口、域名动态分配,或者是 Pod 后台地址动态更新这种问题,就产生了 Ingress 解决方案。

    Ingress 选型

    Nginx Ingress 的缺点

    Ingress 是 Kubernetes 中非常重要的外网流量入口。在Kubernetes中所推荐的默认值为Nginx Ingress,为了与后面Nginx 提供的商业版 Ingress 区分开来,我就称它为Kubernetes Ingress。

    Kubernetes Ingress,顾名思义基于 Nginx 的平台,Nginx 现在是世界上最流行的 Nginx HTTP Sever,相信大家都对 Nginx 也比较熟悉,这是一个优点。它还有一个优点是 Nginx Ingress 接入 Kubernetes 集群所需的配置非常少,而且有很多文档来指引你如何使用它。这对于大部分刚接触 Kubernetes 的人或者创业公司来说,Nginx Ingress 的确是一个非常好的选择。

    但是当 Nginx Ingress 在一些大环境上使用时,就会出现很多问题:
    • 第一个问题:Nginx Ingress用了一些 OpenResty 的特性,但最终配置加载还是依赖于原有的 Nginx config reload。当路由配置非常大时,Nginx reload 会耗时很久,时间长达几秒甚至十几秒,这样就会严重影响业务,甚至造成业务中断。
    • 第二个问题: Nginx Ingress 的插件开发非常困难。如果你认为 Nginx Ingress 本身插件不够用,需要使用一些定制化插件,这个额外的开发任务对程序员来说是十分痛苦的。 因为Nginx Ingress自身的插件能力和可扩展性非常差。


    Ingress 选型原则

    既然发现了 Nginx Ingress 有很多问题,那是不是考虑选择其他开源的、更好用的 Ingress?市场上比 Kubernetes Ingress 好用的Ingress起码有十几家,那么如何从这么多 Ingress 中选择适合自己的呢?

    Ingress 最终是基于 HTTP 网关的,市面上 HTTP 网关主要有这么几种:Nginx、Golang 原生的网关,以及新崛起的 Envoy 。但是每个开发人员所擅长的技术栈不同,所以适合的 Ingress 也会不一样。

    那么问题来了,我们如何选择一个更加好用的 Ingress 呢?或者缩小点范围,熟悉 Nginx 或 OpenResty 的开发人员,应该选择哪一个 Ingress 呢?

    下面来介绍一下我对 Ingress 控制器选型的一些经验。
    2.png

    选型原则

    基本特点

    首先我认为 Ingress 控制器应该具备以下基本功能,如果连这些功能都没有,那完全可以直接pass。
    • 必须开源的,不开源的无法使用。
    • Kubernetes 中 Pod 变化非常频繁,服务发现非常重要。
    • 现在 HTTPS 已经很普及了,TLS 或者 SSL 的能力也非常重要,比如证书管理的功能。
    • 支持 WebSocket 等常见协议,在某些情况下,可能还需要支持 HTTP2、QUIC 等协议。


    基础软件

    前面有提到,每个人擅长的技术平台不一样,所以选择自己更加熟悉的 HTTP 网关也显得至关重要。比如 Nginx、HAProxy、Envoy 或者是 Golang 原生网关。因为你熟悉它的原理,在使用中可以实现快速落地。
    在生产环境上,高性能是一个很重要的特性,但比之更重要的是高可用。这意味着你选择的网关,它的可用性、稳定性一定要非常强,只有这样,服务才能稳定。

    功能需求

    抛开上述两点,就是公司业务对网关的特殊需求。你选择一个开源产品,最好肯定是开箱能用的。比如你需要 GRPC 协议转换的能力,那当然希望选的网关具备这样的功能。这里简单列一下影响选择的因素:
    • 协议:是否支持 HTTP2、HTTP3;
    • 负载均衡算法:最基本的WRR、一致性哈希负载均衡算法是否能够满足需求,还是需要更加复杂的类似EWMA负载均衡算法。
    • 鉴权限流:仅需要简单的鉴权,或更进阶的鉴权方式。又或者需要集成,能够快速的开发出像腾讯云 IM 的鉴权功能。Kubernetes Ingress除了前面我们提到的存在Nginx reload 耗时长、插件扩展能力差的问题,另外它还存在后端节点调整权重的能力不够灵活的问题。


    选择 APISIX 作为 Ingress controller

    相比Kubernetes Ingress,我个人更推荐 APISIX。虽然它在功能上比 Kong 会少很多,但是 APISIX 很好的路由能力、灵活的插件能力,以及本身的高性能,能够弥补在 Ingress 选型上的一些缺点。对于基于 Nginx 或 Openresty 开发的程序员,如果对现在的 Ingress 不满意,我推荐你们去使用 APISIX 作为 Ingress。

    如何将 APISIX 作为 Ingress 呢?我们首先要做出一个区分,Ingress 是 Kubernetes 名称的定义或者规则定义,Ingress controller 是将 Kubernetes 集群状态同步到网关的一个组件。但 APISIX 本身只是 API 网关,怎么把 APISIX 实现成 Ingress controller 呢?我们先来简要了解一下如何实现 Ingress。

    实现 Ingress,本质上就只有两部分内容:
    • 第一部分:需要将 Kubernetes 集群中的配置、或 Kubernetes 集群中的状态同步到 APISIX 集群。
    • 第二部分:需要将 APISIX中 的一些概念,比如像服务、upstream 等概念定义为 Kubernetes 中的 CRD。


    如果实现了第二部分,通过 Kubernetes Ingress 的配置,便可以很快的产生 APISIX。通过 APISIX Ingress controller 就可以产生 APISIX 相关的配置。当前为了快速的将 APISIX 落地为能够支持 Kubernetes 的 Ingress,我们创建了一个开源项目,叫 Ingress controller。
    3.png

    Ingress controller 架构图

    上图为Ingress controller 项目的整体架构图。左边部分为 Kubernetes 集群,这里可以导入一些 yaml 文件,对 Kubernetes 的配置进行变更。右边部分则是 APISIX 集群,以及它的控制面和数据面。从架构图中可以看出,APISIX Ingress 充当了 Kubernetes 集群以及 APISIX 集群之间的连接者。它主要负责监听 Kubernetes 集群中节点的变化,将集群中的状态同步到 APISIX 集群。另外,由于Kubernetes 倡导所有组件都要具备高可用的特性,所以在 APISIX Ingress 设计之初,我们通过双节点或多节点的模式来保证 APISIX Ingress controller 的保障高可用。

    总结

    4.png

    各类 Ingress 横向对比

    相对于市面上流行的 Ingress 控制器,我们简单对比来看看 APISIX Ingress 有什么优缺点。上图是外国开发人员针对 Kubernetes Ingress 选型做的一张表格。我在原来表格的基础上,结合自己的理解,将 APISIX Ingress 的功能加入了进来。我们可以看到,最左边的是APISIX,后边就是 Kubernetes Ingress 和 Kong Ingress,后面的 Traefik,就是基于 Golang 的 Ingress。HAproxy 是比较常见的,过去是比较流行的负载均衡器。Istio 和 Ambassador 是国外非常流行的两个Ingress。
    接下来我们总结下这些 Ingress各自的优缺点:
    • APISIX Ingress:APISIX Ingress 的优点前面也提到了,它具有非常强大的路由能力、灵活的插件拓展能力,在性能上表现也非常优秀。同时,它的缺点也非常明显,尽管APISIX开源后有非常多的功能,但是缺少落地案例,没有相关的文档指引大家如何使用这些功能。
    • Kubernetes Ingress:即 Kubernetes 推荐默认使用的 Nginx Ingress。它的主要优点为简单、易接入。缺点是Nginx reload耗时长的问题根本无法解决。另外,虽然可用插件很多,但插件扩展能力非常弱。
    • Nginx Ingress:主要优点是在于它完全支持 TCP 和 UDP 协议,但是缺失了鉴权方式、流量调度等其他功能。
    • Kong:其本身就是一个 API 网关,它也算是开创了先河,将 API 网关引入到 Kubernetes 中当 Ingress。另外相对边缘网关,Kong 在鉴权、限流、灰度部署等方面做得非常好。Kong Ingress 还有一个很大的优点:提供了一些 API、服务的定义,可以抽象成 Kubernetes 的 CRD,通过Kubernetes Ingress 配置便可完成同步状态至 Kong 集群。缺点就是部署特别困难,另外在高可用方面,与 APISIX 相比也是相形见绌。
    • Traefik:基于 Golang 的 Ingress,它本身是一个微服务网关,在 Ingress 的场景应用比较多。他的主要平台基于 Golang,自身支持的协议也非常多,总体来说是没有什么缺点。如果大家熟悉 Golang 的话,也推荐一用。
    • HAproxy:是一个久负盛名的负载均衡器。它主要优点是具有非常强大的负载均衡能力,其他方面并不占优势。
    • Istio Ingress 和 Ambassador Ingress 都是基于非常流行的 Envoy。说实话,我认为这两个 Ingress 没有什么缺点,可能唯一的缺点是他们基于 Envoy 平台,大家对这个平台都不是很熟悉,上手门槛会比较高。


    综上所述,大家在了解了各个 Ingress 的优劣势后,可以结合自身情况快速选择适合自己的 Ingress。

    作者:厉辉,腾讯云中间件API网关核心研发成员,Apache APISIX PPMC。热爱开源,乐于分享,活跃于Apache APISIX社区。

    原文链接: https://juejin.im/post/5e4684306fb9a07cd74f4b1b

      李飞飞最新访谈:希望AI领域泡沫尽快消散,尤其是医疗部分

      $
      0
      0

      技术与人性间平衡的支点,如何寻找?

      最近,Medscape《医学与机器》栏目的主持人、《深度医学(Deep Medicine)》的作者Eric J. Topol博士与李飞飞围绕“人工智能、医疗、人性”三个关键词展开了交谈。自从离开谷歌回到斯坦福,李飞飞就立即开启了一项“以人为中心的 AI 计划(Human-Centered AI Initiative,HAI)”,李飞飞曾在 twitter 上表示该项目的愿景是:推进 AI 研究、教育、政策和实践,以惠及全人类。

      Eric J. Topol博士:大家好,这里是Eric Topol,也是Medscape《医学与机器》的主持人。很高兴能有机会与斯坦福大学的李飞飞教授面对面交流。她是斯坦福大学Human-Centered人工智能(HAI)研究所的负责人。多年以来,她在人工智能领域拥有巨大的影响力,同时也是我心中的巨人以及挚友。欢迎你,飞飞。

      李飞飞:多谢,Eric。很高兴来到这里。其实我对你也抱有同感,你永远是我心中的医学数字化英雄。

      Topol: 哈哈,太客气啦。 此前,你在斯坦福大学开设了新的研究院能不能结合背景讲讲开设新研究院的原因,以及这家研究院的定位?

      李飞飞你指的应该是斯坦福大学Human-Centered AI研究院。这所研究院当然有很多新鲜元素,但也继承了不少学术机构的特性,毕竟在斯坦福大学乃至美国其他地区,AI技术代表着一大跨学科领域,从业者们也一直在AI社会下的人性问题方面推动着相关探索性研究。除此之外,AI正在与人文、医学、教育乃至其他各类领域开展跨学科合作。

      新的研究院将把这一切结合起来,在我称其为“重大历史时刻”的当下,也是过去十年中的第一次,我们将见证“人工智能”从小众计算机科学议题逐步融入现实生活。此外,我们也将见证AI技术给人类生活以及社会结构造成的深远影响,观察它在各行各业的实际应用,以及可能在无数产品及服务中迸发出的巨大能量。

      无论是从科学性、应用性还是对人类生活的切实影响层面出发,我们都有必要了解AI技术的发展导向,这一点应该得到全世界的高度重视。

      Topol: 毫无疑问,在医学领域,AI将给我们的生活带来更重要的影响。也正因为如此,你才适合在新的研究院当中担任负责人一职。

      多年之前,你曾经创立过名为ImageNet的项目,而该项目也随着时间推移改变了整个AI领域的面貌。从各类图像扫描场景来看,无论是视网膜图像、心电图图像还是其他任何图像,很多从事医学领域并运用这类技术的人们,还不清楚ImageNet正是当前众多医学发展成果的最初萌芽。但我知道,如今的累累硕果,确实始于你当初建立的ImageNet项目。

      你能给大家讲讲当初为什么要建立ImageNet项目,它又在深度学习领域引发了哪些变革吗?

      李飞飞我在AI领域的专业方向,主要是研究计算机视觉与机器学习之间的交集。早在2006年,我就试图解决计算机视觉领域的一个核心难题——用直白的话来说,就是如何实现物体“识别”。

      人类是一种非常聪明的动物,我们会以非常丰富的方式观察这个世界。但是,这种视觉智能的基础,在于首先准确识别出周遭环境中多达几十万种不同的物体,包括小猫、树木、椅子、微波炉、汽车乃至行人等。从这个角度出发探索机器智能的实现,无疑是实现人工智能的第一步,而且直到现在也仍然是重要的一步。

      我们一直在为此努力。当时我还年轻,在学校担任副教授。在评上副教授的第一年,我就开始研究这个问题。但我突然间意识到,那个时代下的所有机器学习算法,在本质上只能处理来自几十种对象的一组极小数据类别。这些数据集中的每个类别只包含100张或者最多几百张图片,这样的素材量远远无法与人类及其他动物的实际成长经历相契合。

      受到人类成长过程的启发,我们意识到大数据对于推动机器学习发展的重要意义。充足的数据量不仅能够改善模式的多样性,同时也在数学层面有着关键的积极意义,能够帮助一切学习系统更好地实现泛化,而非被束缚在总量远低于真实世界的数据集内,经历一次又一次过度拟合。

      以这一观念为基础,我们认为接下来不妨做点疯狂的事情,那就是把我们周遭环境中的所有物体都整理出来。具体是怎么做的?我们受到了英语词汇分类法WordNet的启发,这种方法由语言学家George Miller于上世纪八十年代提出。在WordNet当中,我们能够找到超过8万个用于描述客观对象的名词。

      最终,我们收集到22000个对象类,这些对象类通过不同的搜索引擎从互联网上下载而来。此外,我们还通过Amazon Mechanical Turk发动了规模可观的众包工程项目,这一干就是两年。最终,我们吸引到来自160多个国家和地区的超过5万名参与者,他们帮助我们清理并标记了近10亿张图像,并最终得到一套经过精心规划的数据集。数据集中包含22000个对象类以及下辖的15000万张图像,这就是今天大家所熟悉的ImageNet。

      我们立即把成果向研究社区开源。从2010年开始,我们每年举办一届ImageNet挑战赛,诚邀全球各地的研究人员参与解决这一代表计算机视觉领域终极难题的挑战。

      几年之后,来自加拿大的机器学习研究人员们利用名为“卷积神经网络”这一颇具传统的模型刻了2012年ImageNet挑战赛。没错,就是Geoff Hinton教授带领的小组。

      我知道,很多人都把ImageNet挑战赛视为开启深度学习新时代的里程碑式事件。

      Topol: 大约四年之前,你走上了TED演讲的舞台,谈到我们如何教导计算机理解图片内容。那一期真的很特别,我想至少有几百万人观看过这场演讲。我记得之前曾经问过你,你当时拿出了一张男人骑马的照片,并解释了计算机为什么在对象识别方面一下子就突飞猛进起来。

      但如今的计算机视觉在不同情境下的表现仍然差距巨大,而且偶尔还会犯下错误。我曾问你,我们目前的水平就只能达到这样,对吧?那么现在回头看,我们对于训练机器理解图像内容的理论还准确吗?

      李飞飞是的,虽然图像识别技术一直在稳步发展,但你的观点确实值得深思。图像内容仍存在很多计算机无法判定的细微差别、上下文、背景知识、常识以及推理性元素。这一切,都足以难倒当前最先进的机器智能。单在视觉角度来讲,即使是骑马者雕像这类看似单纯的对象,都同样可能让机器摸不着头脑。

      也可以说, 尽管我们通过大量训练构建起一套能够识别雕塑的系统,它也仍然无法重现人类智能在识别方面的表现具体来讲,人类能够体会到雕塑中的艺术气息、了解背景信息、识别出材质,这一切都是现在的机器智能所无法做到的。

      Topol: 没错。在医学领域,情况也差不多,带有注释的数据集在体量上仍然无法令人满意。由于没有像ImageNet这样包含上千万张经过精心标记的图像,医学从业者只能不断使用相同的数据集训练自己的模型。

      那么,我们有必要投入专门的精力构建新的数据集吗?或者说,自我监督或者无监督学习才代表着真正的未来?

      李飞飞问得好,Eric。相信你也清楚,我认为答案绝不会非此即彼。在大多数情况下,高质量数据集的匮乏确实让医学研究人员与开发人员感到沮丧不已。但在这方面,匮乏也有匮乏的理由。出于患者隐私与安全保护的原因,医疗数据往往需要受到更精心的管理,其中涉及的数据偏见问题也有可能引发更严重的后果。

      在识别小猫时,有点偏见可能没啥大问题。但一旦涉及到人类的生活、健康甚至是福祉时,偏见就会惹出大麻烦。正是由于这些问题,再加上法规的严格限制,导致医学数据很难得到大规模汇总。我认为我们确实有必要为此付出努力,而且目前世界各地的研究人员也开始自发联合起来,共同为攻击这道难关而奋斗。

      与此同时,技术本身的改进也有望从另一个角度带来新的曙光。如你之前提到,除了需要高质量的数据集以训练有监督学习算法之外,机器学习领域在其他一些非常有趣的方向上也取得了重大进展,包括自我监督、迁移学习、联邦学习以及无监督学习等。

      我认为未来这些方法将相互结合。在某些特定场景下,我们仍然需要高质量的大规模数据集。而在另一些场景中,多模与混合数据集可能功效更为显著。

      Topol: 既然医学领域已经掀起了第一波AI浪潮,那么图像识别肯定是最直接的受益场景吧**。 此外,患者与医生也开始与会话式AI系统交互,整个过程终于不用通过键盘实现,而且** 传统录入方式一直是患者跟医生的共同敌人。在你看来,最终临床医生能不能在AI技术的帮助下彻底从枯燥的转录工作中解放出来?

      李飞飞我百分之百相信这一点。这不只是是因为乐观,也是因为我真心希望能够通过AI技术的发展,在医疗保健领域减轻长期以来困扰着临床医生的机械重复负担。

      我的父母也已经年迈,在医院里照顾他们的时候,我曾经认真观察过护士和医生们的工作内容。我认为,在AI领域当中,像我这样的研究者们真心希望帮助他们拿出更多时间来照料病人,而不是把大量精力浪费在盯着屏幕跟图表上。无论是从患者、临床医生还是经济效益的角度出发,我都真心希望这一领域尽快迎来坚实有效的进步。

      Topol: 我对你当时的经历很好奇。你需要照料父母,他们需要在医院里调养身体。那么能不能谈谈切身体会在现在的一流医疗中心里,作为患者家庭成员,你的实际感受如何?

      毕竟归根到底,医疗保健的本质在于由人类照料人类。

      李飞飞首先,作为患者的家属,人类的焦虑、恐惧以及希望等情绪波动仍然非常重要。如果没有个人体验,那么人类很难对事物表现出充分的信任。 我坚信技术能够在这一领域扩大并增强人类的工作能力,但绝对不是要替代人类。我自己的个人研究,以及斯坦福大学HAI研究院的相关工作,一直在以此为核心主题。这一点在医学领域也体现得尤其明显。

      我听说过不少关于AI系统彻底替代医生的讨论,因为机器在某些诊断当中拥有更好的表现。但是在经历了从外科手术到重症监护病房(ICU)的整个医疗体验之后,再加上我在医院里长期看护年迈父母的真实感受,我越来越无法想象没有护士与医生到底会是怎样的情景。

      归根到底,医疗保健的本质在于由人类照料人类。我之所以如此坚定,是因为如果技术能够在处理文书工作方面发挥显著的辅助作用,同时通过更快的早期诊断提升归类效率,那么这样一双保障患者安全的关注之眼才是患者与医护人员真正想要的。我目前所从事的正是这方面的工作,我对这一切也抱有极高的热情。

      Topol: 你与 Arnold Milstein在这方面开展了不少合作,你也开始努力尝试在ICU当中引入机器视觉方案能不能跟我们分享一点经验心得?

      李飞飞Arnold Milstein博士是斯坦福大学的医学教授,同时也是美国医疗领域的意见领袖,一直致力于从研究、政策以及商业实践等不同角度提高医疗质量并削减医疗成本。

      大约七、八年前,她和我偶然相遇,两个人马上就碰撞出了火花。作为AI教授,我感受到深度学习时代的到来,特别是通过无人驾驶汽车、智能传感器、AI算法进步以及相关技术成本下降所带来的全新可能性。我相信,与目前主要依赖人类驾驶员的汽车不同,未来的运输技术将呈现出完全不同的面貌。

      我和Arnold最初的话题就是探讨这项技术,分享心得,进而聊起医疗保健领域的种种问题,包括因系统低效、计时错误、人手不足等问题给患者安全带来的巨大威胁。我们都希望能够让医护工作者将更多时间花在患者身上,并帮助他们在工作中充分享受由智能传感器与AI算法等成果带来的全新体验。

      我们开始讨论如何在医疗保健交付系统的典型场景中,通过智能传感器原型设计为医护人员提供帮助。我们首先确定的场景就是ICU——毕竟作为挽救生命的有力武器,ICU意义重大而且有着极广的社会认知度。患者正在与死神对抗,而我们的临床医生每分每秒都在紧张工作。这个时候,任何小小的失误都有可能改变个人甚至家庭的命运。

      接下来,我们与犹他州Intermountain医院以及斯坦福医院交流,希望了解能否在ICU当中试行患者护理项目,帮助临床医生记录其是否按照规程完成了正确的患者护理(包括口腔护理与复健指导等)。医生们的日常总是忙忙碌碌的,这方面记录工作确实给他们增添了不少负担。

      在项目当中,我们安装了成本低廉的深度传感器,能够在不侵犯患者及临床医生隐私的前提下收集行动数据——之所以安全,是因为其中不涉及任何面部或者身份信息。有了这些数据,我们就能24/7全天候观察患者是否得到妥善护理,并在医疗交付系统中提交反馈。

      我们在斯坦福儿童医院里也做了类似的实验,包括推动手部清洁项目。很多人可能不清楚,由于的手部清洁不当而造成的院内交叉感染,每年在美国造成数千人丧生。同样利用之前提到的低成本传感器以及深度学习算法,我们得以勾勒出临床医生的手部清洁习惯,并发送反馈信息以提醒他们严格按照世界卫生组织提出的标准认真洗手。

      Topol: 确实,利用不起眼的传感器,机器视觉技术确实能够显著提高住院患者的安全性以及治疗效果,这项工作绝对意义深远。我注意到在斯坦福大学的医学中心,你们有专门配备AI的病床,是真的吗?

      李飞飞确实是这样。但目前还处于项目发展早期,我们正在与医院合作,希望进一步扩大传感器项目的部署规模。现在我还没办法讨论具体部署效果,但这项工作已经得到临床医生、医院领导以及AI研究人员们的大力支持。

      尤其让我兴奋的是,我们正在邀请伦理学家、法律学者以及生物伦理学家,共同讨论这项技术可能面临的前沿性挑战。我们希望牢牢把握这方面原则,确保患者、临床医生、病患家属以及其他利益相关者不致因此遭受意外损失。

      Topol: 肯定的,对于医学界来说,实施AI技术的一大基本前提,就是在保证临床医生能够轻松适应的同时,透彻分析各项问题中的深层细节。

      由Pearse Keane领导的一支英国研究团队就发表了一篇论文,他们邀请从未编写过代码也毫无计算机科学背景的医生们尝试使用图像数据集。通过这种方式,医生们逐渐见识到了计算机图像识别的本事。你觉得这是个好主意吗?毕竟我们不可能要求医生自己上阵开发算法,对吧?

      李飞飞我对这个项目不太熟悉。不过,在过去七、八年中,我也一直在进行类似的尝试,研究如何在跨学科小组当中引导医生和计算机科学家共同讨论同一个常规问题。我觉得这段体验让人印象深刻。

      我自己也仍然在学习,目前我得出的一大重要结论,就是人们应当学会相处并理解彼此的工作性质、对方的顾虑、他们的价值主张,同时始终保持耐心与开放的胸怀。当然,接纳对方的专业领域不轻松,也绝对不是什么线性过程,其中有不少问题需要克服。

      我还记得研究团队刚刚成立时,计算机科学家们是如何跟临床医生相互交流的。即使是在今天,当有新的学生或者成员加入时,我们也会组织多轮会议,引导大家尽快熟悉沟通环境。

      作为计算机科学家,我对参加AI医疗保健项目的计算机专业学生们只有一项基本要求——在讨论代码与算法之前,先融入医护人员的日常生活。他们需要进入ICU、病房、手术室甚至是医护人员/患者家中,了解这些人的生活方式并跟他们的家人面对面接触。完成了这一切之后,我们才有资格开始讨论计算机科学问题。

      Topol: 你做出的另一项贡献 当属AI4ALL。通过这个项目,你不仅希望引导计算机科学家与临床医生合作,同时也在尝试培养下一代计算机科学家。快给我们讲讲关于AI4ALL的故事

      李飞飞: 非常感谢,Eric。AI4ALL项目诞生至今已经有五年多时间了。当时是2014年左右,可以说是深度学习革命的发展初期。整个世界,特别是科技领域,尤其是硅谷,全面充斥着对于深度学习技术的兴奋、争论与担忧。

      我很快意识到,大家在面对AI技术时都有些精神分裂——一方面,人们担心终结者和天网变成现实,人类沦为机器的奴隶;另一方面,我们生活在一个极度缺乏多样性的世界当中。在AI专业领域,像我这样的女性真的非常少见。在大多数技术会议当中,女性参会者的比例不足15%。更夸张的是,少数族裔始终存在代表性不足的问题,由于相关统计数据太过有限,我们一直无法建立起有效且可靠的统计结果。

      大家可能觉得这根本就是两码事,但在我看来这两件事之间有着极为深远的关联。作为人类,如果说我们关注AI的理由是关注人类社会的未来,那么我们就必须要关注这样的未来到底由谁所创造。如果AI技术的设计与导向权只掌握在一小部分人手里,那么我们恐怕真的只能接受强大但却根本不打算代表全人类的AI成果。

      我之前带过一名出色的博士生,当时她马上就要博士毕业了。Olga Russakovsky,她现在是普林斯顿大学的AI教授。她和我就这方面担忧进行了深入探讨,我们达成了共识,并决定为此做点什么。

      在2015年到2016年期间,我们在斯坦福大学试行一个新项目,邀请高中女生通过夏令营活动在AI实验室中与我们一道研究人工智能技术。项目取得了巨大的成功,因此在2017年,Melinda Gates(盖茨夫人)与黄仁勋鼓励我们将项目正规化,这就是全国性非营利组织AI4ALL的由来。

      我们的使命是为各行各业教育并启发下一代AI技术专家与意见领袖。AI4ALL现在已经三岁多了。在2019年的夏令营中,我们为北美地区的11所大学在校生准备了时期课程,专门面向少数族裔学生以及公共服务严重短缺的社区——包括低收入家庭学生、农村学生以及女生等等。

      我们仍在为此不懈努力,我们的目标是在未来10到15年之内培养出优秀的继任者,让这些学生在告别校园之后投身技术、特别是AI领域并做出一番成就。目前,我们已经迎来了不少早期案例,效果相当喜人。

      Topol: 在撰写《深度医学(Deep Medicine)》一书的过程中,你的事迹给了我很大的启发。之所以印象深刻,是因为我关注过不少AI领域的大牛,从专泼冷水的批判者到炒作大师应有尽有。但你在其中非常独特,你就像是权衡中的标杆。你一直在努力寻求真理,而且丝毫不避讳AI技术的种种缺点。

      在这里,我想听听你的个人观点。如今的AI技术就像是雨后的春笋,每个礼拜都会出现不少重大事件,来自医学、来自纯AI技术等等。在这一轮上升期当中,必然充斥着大量关于AI技术的炒作之词。那么这一切,特别是与医学相关的消息,到底有多真或者说多假?我们的AI到底发展到什么程度了?

      李飞飞这个话题可就大了。有些人认为如今的AI技术全是泡沫,也有人觉得这些都是真实的结果。在我看来,泡沫确实存在,但AI技术同时也拥有着坚实可靠的内核。而且我始终坚信,AI技术拥有在给医疗保健与医学领域带来深远影响的巨大潜力。

      但说回泡沫,泡沫确实存在,过度夸张、炒作可以说铺天盖地。作为科学家,我希望这些泡沫都尽快消散。只有关注坚实内核的人们才能推动AI进步并带来真正的收益,这一点在医疗保健与医药等领域尤其重要。

      另外,我们绝不应该利用技术制造不公、偏见或者扩大原已存在的不平等现象。对于AI技术,我希望尽可能降低它的接触门槛、增加公平性并缓解种种相关矛盾。只要处理得当,我们完全有机会利用AI技术创造出更美好的未来。当然,前提是我们得认真梳理现有AI成果,弄清哪些是捏造的、哪些是真实的。

      Topol: 非常感谢你参加这次访谈。跟你聊天非常开心,也很高兴能听到你亲口为我解答关于AI技术的种种疑惑。我们将继续关注你和你的团队未来带来的更多成果。

      李飞飞我也会继续关注你的研究,期待你在医学数字化与公共意见表达方面的新进展。感谢你,Eric。

      Topol: 谢谢。

      原文链接:

      https://www.medscape.com/viewarticle/923406?faf=1&src=soc_tw_200208_mscpedt_news_mdscp_medicineandthemachine

      基于kong的微服务解决方案 | kong

      $
      0
      0

      背景

      最近处理了几个客户的需求,需求有相似之处,解决方案迭代几次以后也具备了一定的复制性。分享出来,抛砖引玉。

      需求

      • 目前应用用springboot写的,以业务分块,大概形成了几十个(30+)部署单元;每个部署单元都是独立的jar,其中每个包含十个左右的endpoints
      • 目前用了eureka和zuul做服务注册/发现以及负载均衡;在整体部署规模超过200个jvm之后,出现了一些问题。目前团队整体对eureka和zuul应对更大规模部署信心不足
      • 目前监控主要靠zabbix,对基础设施监控效果很好;但是缺乏对服务级别的监控,包括服务可用性和服务质量
      • 目前还没有部署分布式跟踪系统。尝试过,但是效果一般;实施复杂而且有侵入性
      • 目前日志用ELK套件方案处理,效果不错
      • 需要一个入口的网关,处理认证和访问控制的内容
      • 目前服务之间的服务基本没有管控,但是随着部署的服务越来越多,有计划加强管理,包括访问控制,熔断限流等保障整体服务质量的措施
      • 目前团队在调研基于容器的方案,整体效果还可以,但是容器方案都是全容器方案,对于已有服务的兼容是个问题。但是容器一定会用的;非容器的内容也一定存在
      • 已经有新的功能,团队有意用非java/springboot来开发,包括go和nodejs;服务间的耦合是个新的话题
      • 服务的灰度发布和复杂发布(根据客户属性,路由到指定版本的服务实例)

      方案概述

      diagram

      方案特点

      • 现有程序不用改代码就可以解决上边全部需求
      • 方案兼容容器环境;可以同时支持容器+非容器环境
      • 不用改造,可以实现服务间调用链的管理
      • 不再依赖eureka和zuul
      • 引入了promethus+grafana做了服务质量的监控、告警、服务降级处理
      • 服务网关设计模式的功能主体都实现了(https://microservices.io/patterns/apigateway.html)

      方案讲解

      • 目前的部署单元,不改代码,但是注册eureka的地址,使用kong网关;然后在kong网关配置指向实际的eureka(现实当中是我们用kong实现了eureka的几个api)。这么做的主要因素是,kong在拦截到服务注册时候,可以动态生成路由信息(这里吐槽下,实际本来服务注册就应该和服务路由是一个事情,搞不懂spring cloud分那么多子项目有什么原因…)。简单的说,当有个一个服务的单元注册的时候,注册的信息包括这是哪个服务(spring.application.name),这个服务多了一个提供者(IP+Port),这些信息被拦截之后,动态的添加到了kong的配置里边
      • 服务在调用的之前,先从eureka查询一个可以提供服务的实例,这个请求被kong拦截(拦截的技术手段是正向代理),返回给服务调用方的服务地址是kong网关地址。实际上,地址的域名和ip部分并没有意义;核心的是context,或者说uri的第一个部分,这个部分用于路由
      • 之后服务调用涉及的负载均衡,由kong完成。这改变了基于eureka方案的“客户端负载均衡”的模式;不过实际效果而言,这种半集中的负载均衡方式更简单可靠
      • 之后服务调用因为通过了kong,就可以实现分布式跟踪(open tracing)和服务间访问控制
      • 在kong上通过插件实现了promethus的集成,和zipkin的集成,这样服务间的服务质量(延迟、响应时间、错误代码等)都可以直接获取并且和监控集成
      • 整个服务平台的入口,也使用kong,这样简化了部署和管理。实测大概100~200个jvm实例需要配一个kong实例(不考虑高可用)。所以整个平台扩展到1000jvm需要也就是大概10个kong的网关
      • 在kong上用插件实现了复杂的发布管理(不仅仅是蓝绿发布和灰度发布,实际需求包括根据任意的请求header路由到不同版本的相同后台服务)
      • 向前兼容容器平台。实测采用openshift平台(客户目标平台是基于k8s的容器平台),不需要改变任何openshift的配置,不需要改变任何应用设置,可以支持应用上容器平台,同时可以支持容器+非容器混合使用场景(一部分服务在容器平台上,一部分服务不在容器平台上)
      • 兼容非java的服务实现

      意外收获

      在实现客户诉求以后,通过进一步的分析和实践,我们得到了一些“意外收获”:

      • 之前客户希望实现比“服务”更细力度的管理(可以认为是endpoint,或者我们叫做“API级别”),包括统计数据,包括服务质量和管控。在实施了这个方案之后,我们动态的获取了“服务以下”(就是uri里边第一个路径之后的内容)的统计数据,然后动态的添加了kong的路由,基本实现了这个目的。目前可以实现API级别的统计信息收集和管理
      • 服务调用拓扑的获得。客户开发团队很复杂,水平不尽相同,服务在开发时候很难通过管理手段约束服务定义。通过逆向方式整理出服务的端点和服务拓扑解决了客户的实际痛点

      下一步

      在实现了这些以后,还是有很大空间可以提升的,眼前看到的包括:

      • 我们通过网关,收集了全量的访问数据。目前这些数据被用于“恶意访问”的识别,这种结合机器学习的流量管控手段,可以看成是WAF一些功能点的升级版
      • 进一步收敛部署和维护的复杂度。在数据存储和集成方面,还有空间可以做,这里细节很多,不多说了

      方案对比

      客户也要求我们对比几种他们感兴趣的方案,我们自己也在实施前内部对比和比较过一些细节问题处理,稍微总结一下:

      • 全容器平台。客户计划实施全容器平台,但是仍然有无法容器化的内容。容器是趋势,但是如何过度,是方案的难点之一。在考察过几个容器平台之后,我们作出选择用目前方案。最核心的原因:1. 在没确认捡起来的是西瓜之前,抱着西瓜捡西瓜总比丢了西瓜捡芝麻强;2. 高度集成的容器平台并不太现实,针对分布式/微服务的纯商业产品的解决方案已经跟不上时代发展(其实也没有可以拿出手的商业产品);3. 定制化,细节的定制化,往往可以要了一个方案的命;好的方案一定是核心紧凑、简介,充分性留给定制化,而定制化一定要可控和有效,那种差不多重写整个产品的方案失败是在所难免的
      • istio。我们最纠结的就是是否通过istio满足客户需求。在纠结了半年之后,作出放弃istio的选择还是很艰难的。尽量客观的描述下理由:1. istio刚GA,客户和我们都没有充分信心用这个,在这个时候;2. istio蛮复杂的,现实问题的复杂度和解决方案的复杂度之间的平衡点,是我们最关心的,从描述看istio可以解决各种问题,但是真的缺乏实践作为支撑,需求复杂度和产品简洁之间的沟就是团队成功的关键;3. istio绑定了k8s,尽管参与者都认可k8s,但是绑定怎么说都还是一种风险
      • 公有云。实际这个问题不太具有典型性,有客户从开始就提出要求适用私有数据中心和公有云。简单的回答下这个问题:目前公有云优势明显,但是锁定性还是很强的。目前的方案,既可以适配各种公有云,也可以满足混合环境(多云和公有+私有)的使用诉求
      Viewing all 15905 articles
      Browse latest View live