笨兔儿

笨兔儿 查看完整档案

武汉编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑

java程序转SRE的行者

个人动态

笨兔儿 赞了文章 · 2020-11-27

从kubectl top看K8S监控

一. 前言

kubectl top 可以很方便地查看node、pod的实时资源使用情况:如CPU、内存。这篇文章会介绍其数据链路和实现原理,同时借kubectl top 阐述 k8s 中的监控体系,窥一斑而知全豹。最后会解释常见的一些问题:

  • kubectl top 为什么会报错?
  • kubectl top node 怎么计算,和节点上直接 top 有什么区别?
  • kubectl top pod 怎么计算,包含 pause 吗?
  • kubectl top pod 和exec 进入 pod 后看到的 top 不一样?
  • kubectl top pod 和 docker stats得到的值为什么不同?

以下命令的运行环境为:

  • k8s 1.8
  • k8s 1.13

二. 使用

kubectl top 是基础命令,但是需要部署配套的组件才能获取到监控值

kubectl top node: 查看node的使用情况

kubectl top pod: 查看 pod 的使用情况

不指定pod 名称,则显示命名空间下所有 pod,--containers可以显示 pod 内所有的container

指标含义:

  • 和k8s中的request、limit一致,CPU单位100m=0.1 内存单位1Mi=1024Ki
  • pod的内存值是其实际使用量,也是做limit限制时判断oom的依据。pod的使用量等于其所有业务容器的总和,不包括 pause 容器,值等于cadvisr中的container_memory_working_set_bytes指标
  • node的值并不等于该node 上所有 pod 值的总和,也不等于直接在机器上运行 top 或 free 看到的值

三. 实现原理

3.1 数据链路

kubectl top 、 k8s dashboard 以及 HPA 等调度组件使用的数据是一样,数据链路如下:

使用 heapster 时:apiserver 会直接将metric请求通过 proxy 的方式转发给集群内的 hepaster 服务。

而使用 metrics-server 时:apiserver是通过/apis/metrics.k8s.io/的地址访问metric

这里可以对比下kubect get pod时的日志:

3.2 metric api

可以发现,heapster使用的是 proxy 转发,而 metric-server 和普通 pod都是使用 api/xx 的资源接口,heapster采用的这种 proxy 方式是有问题的:

    1. proxy只是代理请求,一般用于问题排查,不够稳定,且版本不可控
    1. heapster的接口不能像apiserver一样有完整的鉴权以及client集成,两边都维护的话代价高,如generic apiserver
    1. pod 的监控数据是核心指标(HPA调度),应该和 pod 本身拥有同等地位,即 metric应该作为一种资源存在,如metrics.k8s.io 的形式,称之为 Metric Api

于是官方从 1.8 版本开始逐步废弃 heapster,并提出了上边Metric api 的概念,而metrics-server 就是这种概念下官方的一种实现,用于从 kubelet获取指标,替换掉之前的 heapster

3.3 kube-aggregator

有了metrics-server组件,采集到了需要的数据,也暴露了接口,但走到这一步和 heapster 其实没有区别,最关键的一步就是如何将打到apiserver的/apis/metrics.k8s.io请求转发给metrics-server组件?解决方案就是:kube-aggregator

kube-aggregator是对 apiserver 的有力扩展,它允许k8s的开发人员编写一个自己的服务,并把这个服务注册到k8s的api里面,即扩展 API,metric-server其实在 1.7版本就已经完成了,只是在等kube-aggregator的出现。

kube-aggregator是 apiserver 中的实现,有些 k8s 版本默认没开启,你可以加上这些配置
来开启。他的核心功能是动态注册、发现汇总、安全代理。

如metric-server注册 pod 和 node 时:

3.4 监控体系

在提出 metric api 的概念时,官方页提出了新的监控体系,监控资源被分为了2种:

  • Core metrics(核心指标):从 Kubelet、cAdvisor 等获取度量数据,再由metrics-server提供给 Dashboard、HPA 控制器等使用。
  • Custom Metrics(自定义指标):由Prometheus Adapter提供API custom.metrics.k8s.io,由此可支持任意Prometheus采集到的指标。

核心指标只包含node和pod的cpu、内存等,一般来说,核心指标作HPA已经足够,但如果想根据自定义指标:如请求qps/5xx错误数来实现HPA,就需要使用自定义指标了。

目前Kubernetes中自定义指标一般由Prometheus来提供,再利用k8s-prometheus-adpater聚合到apiserver,实现和核心指标(metric-server)同样的效果。

3.5 kubelet

前面提到,无论是 heapster还是 metric-server,都只是数据的中转和聚合,两者都是调用的 kubelet 的 api 接口获取的数据,而 kubelet 代码中实际采集指标的是 cadvisor 模块,你可以在 node 节点访问 10255 端口 (read-only-port)获取监控数据:

  • Kubelet Summary metrics: 127.0.0.1:10255/metrics,暴露 node、pod 汇总数据
  • Cadvisor metrics: 127.0.0.1:10255/metrics/cadvisor,暴露 container 维度数据

示例,容器的内存使用量:

kubelet虽然提供了 metric 接口,但实际监控逻辑由内置的cAdvisor模块负责,演变过程如下:

  • 从k8s 1.6开始,kubernetes将cAdvisor开始集成在kubelet中,不需要单独配置
  • 从k8s 1.7开始,Kubelet metrics API 不再包含 cadvisor metrics,而是提供了一个独立的 API 接口来做汇总
  • 从k8s 1.12开始,cadvisor 监听的端口在k8s中被删除,所有监控数据统一由Kubelet的API提供

到这里为止,k8s范围内的监控体系就结束了,如果你想继续了解cadvisor和 cgroup 的内容,可以向下阅读

3.6 cadvisor

cadvisor由谷歌开源,使用Go开发,项目地址也是google/cadvisor,cadvisor不仅可以搜集一台机器上所有运行的容器信息,包括CPU使用情况、内存使用情况、网络吞吐量及文件系统使用情况,还提供基础查询界面和http接口,方便其他组件进行数据抓取。在K8S中集成在Kubelet里作为默认启动项,k8s官方标配。

cadvisor 拿到的数据结构示例:

核心逻辑:

通过new出来的memoryStorage以及sysfs实例,创建一个manager实例,manager的interface中定义了许多用于获取容器和machine信息的函数

cadvisor的指标解读:cgroup-v1

cadvisor获取指标时实际调用的是 runc/libcontainer库,而libcontainer是对 cgroup文件 的封装,即 cadvsior也只是个转发者,它的数据来自于cgroup文件。

3.7 cgroup

cgroup文件中的值是监控数据的最终来源,如

  • mem usage的值,来自于

/sys/fs/cgroup/memory/docker/[containerId]/memory.usage_in_bytes

  • 如果没限制内存,Limit = machine_mem,否则来自于

/sys/fs/cgroup/memory/docker/[id]/memory.limit_in_bytes

  • 内存使用率 = memory.usage_in_bytes/memory.limit_in_bytes

一般情况下,cgroup文件夹下的内容包括CPU、内存、磁盘、网络等信息:


如memory下的几个常用的指标含义:

memory.stat中的信息是最全的:

原理到这里结束,这里解释下最开始的kubectl top 的几个问题:

四. 问题

4.1 kubectl top 为什么会报错

一般情况下 top 报错有以下几种,可以 kubectl top pod -v=10看到具体的调用日志

  1. 没有部署 heapster 或者 metric-server,或者pod运行异常,可以排查对应 pod日志
  2. 要看的pod 刚刚建出来,还没来得及采集指标,报 not found 错误,默认1 分钟
  3. 以上两种都不是,可以检查下kubelet 的 10255 端口是否开放,默认情况下会使用这个只读端口获取指标,也可以在heapster或metric-server的配置中增加证书,换成 10250 认证端口

4.2 kubectl top pod 内存怎么计算,包含 pause容器 吗

每次启动 pod,都会有一个 pause 容器,既然是容器就一定有资源消耗(一般在 2-3M 的内存),cgroup 文件中,业务容器和 pause 容器都在同一个 pod的文件夹下。

但 cadvisor 在查询 pod 的内存使用量时,是先获取了 pod 下的container列表,再逐个获取container的内存占用,不过这里的 container 列表并没有包含 pause,因此最终 top pod 的结果也不包含 pause 容器

pod 的内存使用量计算

kubectl top pod 得到的内存使用量,并不是cadvisor 中的container_memory_usage_bytes,而是container_memory_working_set_bytes,计算方式为:

  • container_memory_usage_bytes == container_memory_rss + container_memory_cache + kernel memory
  • container_memory_working_set_bytes = container_memory_usage_bytes - total_inactive_file(未激活的匿名缓存页)

container_memory_working_set_bytes是容器真实使用的内存量,也是limit限制时的 oom 判断依据

cadvisor 中的 container_memory_usage_bytes对应 cgroup 中的 memory.usage_in_bytes文件,但container_memory_working_set_bytes并没有具体的文件,他的计算逻辑在 cadvisor 的代码中,如下:

同理,node 的内存使用量也是container_memory_working_set_bytes

4.3 kubectl top node 怎么计算,和节点上直接 top 有什么区别

kubectl top node得到的 cpu 和内存值,并不是节点上所有 pod 的总和,不要直接相加。top node是机器上cgroup根目录下的汇总统计

在机器上直接 top命令看到的值和 kubectl top node 不能直接对比,因为计算逻辑不同,如内存,大致的对应关系是(前者是机器上 top,后者是kubectl top):

rss + cache = (in)active_anon + (in)active_file

4.4 kubectl top pod 和exec 进入 pod 后看到的 top 不一样

top命令的差异和上边 一致,无法直接对比,同时,就算你对 pod 做了limit 限制,pod 内的 top 看到的内存和 cpu总量仍然是机器总量,并不是pod 可分配量

  • 进程的RSS为进程使用的所有物理内存(file_rss+anon_rss),即Anonymous pages+Mapped apges(包含共享内存)
  • cgroup RSS为(anonymous and swap cache memory),不包含共享内存。两者都不包含file cache

4.5 kubectl top pod 和 docker stats得到的值为什么不同?

docker stats dockerID 可以看到容器当前的使用量:


如果你的 pod中只有一个 container,你会发现docker stats 值不等于kubectl top 的值,既不等于 container_memory_usage_bytes,也不等于container_memory_working_set_bytes。

因为docker stats 和 cadvisor 的计算方式不同,总体值会小于 kubectl top:计算逻辑是:

docker stats = container_memory_usage_bytes - container_memory_cache

五. 后记

一般情况下,我们并不需要时刻关心node 或 pod 的使用量,因为有集群自动扩缩容(cluster-autoscaler)和pod 水平扩缩容(HPA)来应对这两种资源变化,资源指标的意义更适合使用prometheus来持久化 cadvisor 的数据,用于回溯历史或者发送报警。

关于prometheus的内容可以看容器监控系列

其他补充:

  1. 虽然kubectl top help中显示支持Storage,但直到 1.16 版本仍然不支持
  2. 1.13 之前需要 heapster,1.13 以后需要metric-server,这部分kubectl top help的输出 有误,里面只提到了heapster
  3. k8s dashboard 中的监控图默认使用的是 heapster,切换为 metric-server后数据会异常,需要多部署一个metric-server-scraper 的 pod 来做接口转换,具体参考 pr:https://github.com/kubernetes...

六. 参考资料

查看原文

赞 1 收藏 0 评论 0

笨兔儿 发布了文章 · 2020-11-27

本地mac系统通过minikube安装k8s集群

环境准备

  • 安装配置minikube
# 下载对应系统的包
https://github.com/kubernetes/minikube/releases/tag/v1.15.1
# 配置环境变量
vim /etc/profile
export MINIKUBE_PATH=/Users/zengqiang/soft/minikube/
export PATH=$MINIKUBE_PATH:$PATH
# 重载配置
source /etc/profile
  • 安装轻量级的vm

hyperkitgithub地址,传送门 安装需要点时间耐心等待安装完成

brew install hyperkit
  • 安装docker默认使用官方安装指南即可

minikube安装k8s集群

  • 使用minikube安装k8s

这边启动驱动选择使用hyperkit安装需要时间耐心等待安装完成

minikube start --dirver=hyperkit
  • 检验k8s安装情况
kubectl get pod -A
minikube dashboard
查看原文

赞 0 收藏 0 评论 0

笨兔儿 发布了文章 · 2019-11-19

filebeat 7.4 codec.format 变量配置方法

filebeat 7.4 codec.format 变量配置方法

无论你是使用哪个版本的filebeat,当你的output.xx中需要配置code.cormat的变量,最开始是从官网上查询,后来发现有个更方便的方法;
重点就是codec.format中的变量都是从已经生成json的对象中获取,就是信息和元数据都已经生成好准备output目标;
  • 打印outputjson数据
{
    "@timestamp":"2019-11-19T06:42:34.222Z",
    "@metadata":{
        "beat":"filebeat",
        "type":"_doc",
        "version":"7.4.2",
        "topic":"xxx-click"
    },
    "host":{
        "name":"eeexx-asdddd"
    },
    "agent":{
        "version":"7.4.2",
        "type":"filebeat",
        "ephemeral_id":"3969b5f3-9aa2-4c5a-b8bd-971017f480c5",
        "hostname":"eeexx-asdddd",
        "id":"10b92cfc-b641-4dde-a72f-a599e178e05d"
    },
    "log":{
        "offset":48845307,
        "file":{
            "path":"/Log-xxxx.log"
        }
    },
    "message":"2019-11-19 14:42:33.830 INFO  .0 (iPhone; CPU iPhone OS 13_1_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148" "" """,
    "input":{
        "type":"log"
    },
    "fields":{
        "log_topic":"xxx-click"
    },
    "ecs":{
        "version":"1.1.0"
    }
}
  • 根据你所需要的配置codec.format
output.kafka:
  codec.format: '%{[fields.log_topic]} %{[agent.hostname]}'

output.kafka 输出信息异常

`client/metadata fetching metadata for [xx-play] from broker 172.16.xx.xx:9092
kafka message: client/metadata found some partitions to be leaderless` 异常信息

  • 异常原因kafka中没有创建xx-play topic创建好之后就能正常输出到topic
  • 发生这样异常信息时,如果有多个output也会有影响,不能正常out 信息
查看原文

赞 0 收藏 0 评论 0

笨兔儿 发布了文章 · 2019-02-27

Prometheus 500 Internal Privoxy Error 异常解决

Prometheus 500 Internal Privoxy Error 异常解决

  • 访问Prometheus web-ui时异常
Privoxy encountered an error while processing your request:
Could not load template file no-server-data or one of its included components.
Please contact your proxy administrator.
If you are the proxy administrator, please put the required file(s)in the (confdir)/templates directory. The location of the (confdir) directory is specified in the main Privoxy config file. (It's typically the Privoxy install directory).
  • 解决方案

翻墙软件因开全局代理导致异常,将全局代理修改为自动代理就解决

查看原文

赞 0 收藏 0 评论 0

笨兔儿 发布了文章 · 2019-01-18

Prometheus HA详解

Prometheus HA详解

以下所有操作都是在k8s集群中完成,如果你是VM或者物理机在配置方面不会有太大区别;

Prometheus 横向扩展

Exporter或者采集信息需要越来越多时就会考虑高可用,高可用优点不会因为集群中某个节点down而导致Prometheus不可用,可以让算力下沉;
缺点是A-PrometheusB-Prometheus这两个实例会定时去scrape数据,并且存储在各本地,这样导致数据会存储两份;

clipboard.png

  • 高可用配置

Prometheus启动两个实例,配置一样只需要暴露的service的端口不同,'Nginx Controller'配置session-affinityservice名称;

Prometheus 联邦

在多个数据中心部署Prometheus需要将多数据中心数据合在一起管理,使用联邦模式非常合适,如果担心数据单点,可以在联邦的基础上再扩展高可用;
优点集中式管理数据,报警,不需要为每个Prometheus实例管理数据,如有些敏感节点报警要求高可以在Prometheus数据节点上加报警信息,可以按功能环境划分启动多个Prometheus采集实例;
缺点数据集中化,网络可能会延时,数据单点等问题;

clipboard.png

终级解决方案

Prometheus 是支持远程读写TSDB数据库,请看官方网站支持哪些数据库的读写,因为有些数据只支持写而不支持读,你内网搭建TSDB集群,你所有启动的Prometheus实例都把数据写入到远程数据库,再使用高可用方案支持查询,只支持远程读,这样就可无限扩展采集实例和查询实例,非常的爽,作者没有实践过只是YY中;

  • 采集的Metrics远程写入TSDB

clipboard.png

  • Prometheus远程读TSDB

clipboard.png

文章会持续更新,文章中有不好之处欢迎留言

我的博客即将同步至 OSCHINA 社区,这是我的 OSCHINA ID:osc_ouf9dku6,邀请大家一同入驻:https://www.oschina.net/shari...

查看原文

赞 1 收藏 1 评论 0

笨兔儿 发布了文章 · 2019-01-09

pushgateway on k8s 部署yaml

pushgateway on k8s 部署yaml

prometheus pushgateway部署的yaml文件

  • pushgatewaydeployment文件内容
apiVersion: apps/v1beta2
kind: Deployment
metadata:
  namespace: kube-ops
  name:  pushgateway-ttt
  labels:
    app:  pushgateway-ttt
  annotations:
    prometheus.io/scrape: "true"
    prometheus.io/port: "8080"
spec:
  replicas: 1
  revisionHistoryLimit: 0
  selector:
    matchLabels:
      app:  pushgateway-ttt
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: "25%"
      maxUnavailable: "25%"
  template:
    metadata:
      name:  pushgateway-ttt
      labels:
        app:  pushgateway-ttt
    spec:
      containers:
        - name:  pushgateway-ttt
          image: prom/pushgateway:v0.7.0
          imagePullPolicy: IfNotPresent
          livenessProbe:
            initialDelaySeconds: 600
            periodSeconds: 10
            successThreshold: 1
            failureThreshold: 10
            httpGet:
              path: /
              port: 9091
          ports:
            - name: "app-port"
              containerPort: 9091
          resources:
            limits:
              memory: "1000Mi"
              cpu: 1
            requests:
              memory: "1000Mi"
              cpu: 1

prom/pushgateway可以在hub.docker.io查到dockerfile文件及部署说明,启动参数--persistence.file等都是放入spec.args中添加即可生效;
所有启动参数:

--web.listen-address default:9091
--web.telemetry-path  default:/metrics
--web.route-prefix default:""
--persistence.file default: ""
--persistence.interval default: 5m

参数说明地址

  • pushgatewayservice文件内容
apiVersion: v1
kind: Service
metadata:
  name: pushgateway-ttt
  namespace: kube-ops
  labels:
    app: pushgateway-ttt
spec:
  selector:
    app: pushgateway-ttt
  #type: NodePort
  ports:
    - name: pushgateway-ttt
      port: 9091
      targetPort: 9091
  • pushgatewayingress文件内容
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: pushgateway-ingress
  namespace: kube-ops
  annotations:
    kubernetes.io/ingress.class: nginx
spec:
  rules:
  - host: push-prometheus.ttt.mucang.cn
    http:
      paths:
      - path: /
        backend:
          serviceName: pushgateway-ttt
          servicePort: 9091

Prometheus中配置pushgatewaypull pushgateway数据配置

# prometheus配置文件中配置pull pushgateway组件配置
- job_name: 'pushgateway'
  scrape_interval: 60s
  metrics_path: /metrics
  static_configs:
  - targets: ["push-prometheus.xxx.xx.xx]
# prometheus的deployment配置文件配置环境变量,这样prometheus才会去pull pushgateway缓存的数据
# 所有推到pushgateway的数据都会在/metrics接口的体现
查看原文

赞 0 收藏 0 评论 0

笨兔儿 评论了文章 · 2019-01-09

ubuntu 织梦DEDE安装 GD插件 OFF问题

Ubuntu 16.04下安装织梦CMS系统

安装Ubuntu 16.04安装织梦CMS开始发现所有目录没有权限,把源码的用户加入到php用户组下面,目录权限问题解决,接下来php-mysql显示为off,安装好php-mysql后,php-gd显示为off找了半天无解,最后在google下找到解决方案,但是与我碰到问题不一样,最后修改代码成功运行将off显示为on

问题描述

图片描述

解决方案

  • 查看`php5.7-gd` 版本,如果没有安装php5.7-gd请先根据你的php版本安装gd
# php --ri gd
GD Support => enabled
GD headers Version => 2.2.5
GD library Version => 2.2.5
FreeType Support => enabled
FreeType Linkage => with freetype
FreeType Version => 2.6.1
GIF Read Support => enabled
GIF Create Support => enabled
JPEG Support => enabled
libJPEG Version => 8
PNG Support => enabled
libPNG Version => 1.2.54
WBMP Support => enabled
XPM Support => enabled
libXpm Version => 30411
XBM Support => enabled
WebP Support => enabled

Directive => Local Value => Master Value
gd.jpeg_ignore_warning => 0 => 0
  • 因为install/install.inc.php 中返回的版本是2.0,需要将返回版本修改你安装的真实版本2.1.1
function gdversion()
{
  //没启用php.ini函数的情况下如果有GD默认视作2.0以上版本
  //因为我是ubuntu服务器,没有配置php.ini,所以这里返回的是GD2.0版本,然而我的GD版本为2.1.1,所以手动配置版本号
  if(!function_exists('phpinfo'))
  {
      //if(function_exists('imagecreate')) return '2.0';
      if(function_exists('imagecreate')) return '2.1.1';

      else return 0;
  }
  else
  {
    ob_start();
    phpinfo(8);
    $module_info = ob_get_contents();
    ob_end_clean();
    if(preg_match("/\bgd\s+version\b[^\d\n\r]+?([\d\.]+)/i", $module_info,$matches)) {   $gdversion_h = $matches[1];  }
    else {  $gdversion_h = 0; }
    // return $gdversion_h;
    return '2.1.1';
  }
  • 查看结果

图片描述

查看原文

笨兔儿 发布了文章 · 2019-01-08

mac os 安装 iterm2+fish shell 终端配色及vim语法高亮

mac os 安装 iterm2+fish shell 终端配色及vim语法高亮

安装iterm2fish shell

  • 安装 iterm2

iterm2软件下载地址,直接安装dmg文件

  • 安装 fish shell

fish shell说明及下载地址,根据自己熟悉方式安装fish shell

将fish shell添加到mac的shell列表里,用shell命令sudo vim /etc/shells把/usr/local/bin/fish 追加到文件尾处;
最后,按esc键跳到命令模式,输入:wq命令保存文件,使用 chsh -s /usr/local/bin/fish 切换到fish shell;
打开iterm2时自动打开fish shell,vim ~/bash_profile在文件尾处添加fish命令,使用:wq保存文件即可;
  • vim语法高亮
首先开启语法高亮功能,如果没有这个文件就创建文件:
vim ~/.vimrc
在.vimrc文件尾处添加如下代码:
syntax on
  • vim选择主题
vim 有更多的主题可以选择,使用如下命令可以查看vim所有的color主题:
cd /usr/share/vim/vim73/colors/ && ls

在.vimrc文件中添加vim的color主题,命令如下:
vim ~/.vimrc
添加colorscheme darkblue信息到文件尾处

以上所有配置想看效果需要重新打开iterm2,如文中有错误烦请留言指出,谢谢

查看原文

赞 0 收藏 0 评论 0

笨兔儿 发布了文章 · 2018-12-19

Golang-filepath使用

Golang-filepath 使用

获取当前目录

os.GetPWD()
filepath.Abs(path) # 绝对目录
filepath.Dir(path) # 相对目录
可以 filepath.Abs(filepath.Dir(path))

获取字符目录,前缀,后缀等方法

filepath.Split(path)
filepath.Base(path) # test.txt
filepath.Ext(path) # .txt
查看原文

赞 0 收藏 0 评论 0

笨兔儿 赞了文章 · 2018-11-26

Pod Preset玩转K8S容器时区自动配置

摘要: 通过Pod Preset自动配置容器的时区

缘由

默认的情况,在K8S里启动一个容器,该容器的设置的时区是UTC0,但是对于很多客户而言,其主机环境并不在UTC0。例如中国客户在UTC8。如果不把容器的时区和主机主机设置为一致,则在查找日志等时候将非常不方便,也容易造成误解。但是K8S以及Docker容器没有一个简便的设置/开关在系统层面做配置。都需要我们从单个容器入手做设置,具体方法如下:

纯Docker的时区设置方式

Docker引擎提供了两种设置方式,分别是通过环境变量以及挂载主机文件方式来完成

方式一:设置容器的时区环境变量

先看看没有设置前,容器的情况:

clipboard.png

clipboard.png

从输出可以看出,容器和主机差了8个时区

我们通过环境变量的方式来改变容器的时区:

clipboard.png

clipboard.png

从输出可以看出,虽然没有改变对应的localtime文件,但是容器和主机的时区是一致的了。

方式二:挂载主机的时区文件到容器中

clipboard.png

clipboard.png

从输出可以看出,容器和主机的时区是一致的了,而且使用了本机的时区文件。

Kubernetes的时区设置方式

在K8S中,可以参考Docker的方式进行设置

通过环境变量设置

clipboard.png

通过挂载主机时区文件设置

clipboard.png

这里引出了一个问题,难道每次每个容器都要做这样的配置才可以么?可否在系统层面设置,而无需在对应yaml文件体现呢?不然yaml文件将过于啰嗦。答案是使用K8S的特性Pod Preset来控制容器启动前先配置好对应时区环境变量,或者挂载主机文件。下面我们通过配置环境变量的方式说明,挂载文件是类似的,就不重复了。

通过Pod Preset预设置时区环境变量

激活Pod Preset

Pod Preset目前还是alpha阶段,默认是没有激活的,所以需要通过以下步骤激活:

咱们以阿里云的Kubernetes服务为例(如果还没有,可以尝试一下,一键就可以开通,还免费哦)。阿里云的Kubernetes服务的master组件(API Server, Scheduler, Controller)都是通过Static Pod的方式用Kubelet启动,所以需要更改对应的yaml来激活Pod Preset:

编辑/etc/kubernetes/manifests/kube-apiserver.yaml,

• 在-runtime-config增加settings.k8s.io/v1alpha1=true

• 在--admission-control增加PodPreset`

保存后kubelet会自动重启kube-apiserver组件。我们需要同时更改3台机器的master才可以。在期间你将收到类似以下的告警,在api server重启成功后会自动恢复

clipboard.png

验证配置成功

• 确保api server已经恢复,如果恢复将收到如下通知信息:

clipboard.png

• kubectl可以查询Pod Preset。在开关没有开启成功前,是无法调用以下命令的

clipboard.png

配置设置时区的Pod Preset

对应的Pod Preset对象创建文件如下:

clipboard.png

这里需要注意的地方是,一定需要写selector...matchLabels,但是matchLabels为空,标示应用于所有容器,这个正式我们所期望的

clipboard.png

clipboard.png

可以得到创建成功的Pod Preset列表:

clipboard.png

以普通的方式创建容器,但是环境变量被Pod Preset注入了

clipboard.png

进入容器看看对应的环境变量:

clipboard.png

从输出可以看出,容器已经被默认配置了时区的环境变量,对应的时区是Asia/Shanghai

小结

至此,我们就完成了容器的时区的"自动"配置了。Pod Preset的预设功能还是非常便利的,目前这块还在演进中,但是已经能大大简化了相关的管理工作,将这些配置从开发者手中解脱出来,变成系统管理配置。

需要注意的是,Pod Preset是namespace级别的对象,其作用范围只能是同一个命名空间下容器。

给社区的建议

建议Kubernetes社区可以在kubelet的启动参数重,增加一个开关,用来设置容器的默认时区,毕竟这个是很常见的实践

参考文档

Pod Preset: https://kubernetes.io/docs/co...

Static Pod: https://kubernetes.io/docs/ta...

label-selectors: https://kubernetes.io/docs/co...

本文作者:了哥-duff

阅读原文

本文为云栖社区原创内容,未经允许不得转载。

查看原文

赞 1 收藏 0 评论 0

笨兔儿 评论了文章 · 2018-11-21

Token 认证的来龙去脉

不久前,我在在前后端分离实践中提到了基于 Token 的认证,现在我们稍稍深入一些。

通常情况下,我们在讨论某个技术的时候,都是从问题开始。那么第一个问题:

为什么要用 Token?

而要回答这个问题很简单——因为它能解决问题!

可以解决哪些问题呢?

  1. Token 完全由应用管理,所以它可以避开同源策略
  2. Token 可以避免 CSRF 攻击
  3. Token 可以是无状态的,可以在多个服务间共享

Token 是在服务端产生的。如果前端使用用户名/密码向服务端请求认证,服务端认证成功,那么在服务端会返回 Token 给前端。前端可以在每次请求的时候带上 Token 证明自己的合法地位。如果这个 Token 在服务端持久化(比如存入数据库),那它就是一个永久的身份令牌。

于是,又一个问题产生了:需要为 Token 设置有效期吗?

需要设置有效期吗?

对于这个问题,我们不妨先看两个例子。一个例子是登录密码,一般要求定期改变密码,以防止泄漏,所以密码是有有效期的;另一个例子是安全证书。SSL 安全证书都有有效期,目的是为了解决吊销的问题,对于这个问题的详细情况,来看看知乎的回答。所以无论是从安全的角度考虑,还是从吊销的角度考虑,Token 都需要设有效期。

那么有效期多长合适呢?

只能说,根据系统的安全需要,尽可能的短,但也不能短得离谱——想像一下手机的自动熄屏时间,如果设置为 10 秒钟无操作自动熄屏,再次点亮需要输入密码,会不会疯?如果你觉得不会,那就亲自试一试,设置成可以设置的最短时间,坚持一周就好(不排除有人适应这个时间,毕竟手机厂商也是有用户体验研究的)。

然后新问题产生了,如果用户在正常操作的过程中,Token 过期失效了,要求用户重新登录……用户体验岂不是很糟糕?

为了解决在操作过程不能让用户感到 Token 失效这个问题,有一种方案是在服务器端保存 Token 状态,用户每次操作都会自动刷新(推迟) Token 的过期时间——Session 就是采用这种策略来保持用户登录状态的。然而仍然存在这样一个问题,在前后端分离、单页 App 这些情况下,每秒种可能发起很多次请求,每次都去刷新过期时间会产生非常大的代价。如果 Token 的过期时间被持久化到数据库或文件,代价就更大了。所以通常为了提升效率,减少消耗,会把 Token 的过期时保存在缓存或者内存中。

还有另一种方案,使用 Refresh Token,它可以避免频繁的读写操作。这种方案中,服务端不需要刷新 Token 的过期时间,一旦 Token 过期,就反馈给前端,前端使用 Refresh Token 申请一个全新 Token 继续使用。这种方案中,服务端只需要在客户端请求更新 Token 的时候对 Refresh Token 的有效性进行一次检查,大大减少了更新有效期的操作,也就避免了频繁读写。当然 Refresh Token 也是有有效期的,但是这个有效期就可以长一点了,比如,以天为单位的时间。

时序图表示

使用 Token 和 Refresh Token 的时序图如下:

1)登录

clipboard.png

2)业务请求

clipboard.png

3)Token 过期,刷新 Token

clipboard.png

上面的时序图中并未提到 Refresh Token 过期怎么办。不过很显然,Refresh Token 既然已经过期,就该要求用户重新登录了。

当然还可以把这个机制设计得更复杂一些,比如,Refresh Token 每次使用的时候,都更新它的过期时间,直到与它的创建时间相比,已经超过了非常长的一段时间(比如三个月),这等于是在相当长一段时间内允许 Refresh Token 自动续期。

到目前为止,Token 都是有状态的,即在服务端需要保存并记录相关属性。那说好的无状态呢,怎么实现?

无状态 Token

如果我们把所有状态信息都附加在 Token 上,服务器就可以不保存。但是服务端仍然需要认证 Token 有效。不过只要服务端能确认是自己签发的 Token,而且其信息未被改动过,那就可以认为 Token 有效——“签名”可以作此保证。平时常说的签名都存在一方签发,另一方验证的情况,所以要使用非对称加密算法。但是在这里,签发和验证都是同一方,所以对称加密算法就能达到要求,而对称算法比非对称算法要快得多(可达数十倍差距)。更进一步思考,对称加密算法除了加密,还带有还原加密内容的功能,而这一功能在对 Token 签名时并无必要——既然不需要解密,摘要(散列)算法就会更快。可以指定密码的散列算法,自然是 HMAC。

上面说了这么多,还需要自己去实现吗?不用!JWT 已经定义了详细的规范,而且有各种语言的若干实现。

不过在使用无状态 Token 的时候在服务端会有一些变化,服务端虽然不保存有效的 Token 了,却需要保存未到期却已注销的 Token。如果一个 Token 未到期就被用户主动注销,那么服务器需要保存这个被注销的 Token,以便下次收到使用这个仍在有效期内的 Token 时判其无效。有没有感到一点沮丧?

在前端可控的情况下(比如前端和服务端在同一个项目组内),可以协商:前端一但注销成功,就丢掉本地保存(比如保存在内存、LocalStorage 等)的 Token 和 Refresh Token。基于这样的约定,服务器就可以假设收到的 Token 一定是没注销的(因为注销之后前端就不会再使用了)。

如果前端不可控的情况,仍然可以进行上面的假设,但是这种情况下,需要尽量缩短 Token 的有效期,而且必须在用户主动注销的情况下让 Refresh Token 无效。这个操作存在一定的安全漏洞,因为用户会认为已经注销了,实际上在较短的一段时间内并没有注销。如果应用设计中,这点漏洞并不会造成什么损失,那采用这种策略就是可行的。

在使用无状态 Token 的时候,有两点需要注意:

  1. Refresh Token 有效时间较长,所以它应该在服务器端有状态,以增强安全性,确保用户注销时可控
  2. 应该考虑使用二次认证来增强敏感操作的安全性

到此,关于 Token 的话题似乎差不多了——然而并没有,上面说的只是认证服务和业务服务集成在一起的情况,如果是分离的情况呢?

分离认证服务

当 Token 无状态之后,单点登录就变得容易了。前端拿到一个有效的 Token,它就可以在任何同一体系的服务上认证通过——只要它们使用同样的密钥和算法来认证 Token 的有效性。就样这样:

clipboard.png

当然,如果 Token 过期了,前端仍然需要去认证服务更新 Token:

clipboard.png

可见,虽然认证和业务分离了,实际即并没产生多大的差异。当然,这是建立在认证服务器信任业务服务器的前提下,因为认证服务器产生 Token 的密钥和业务服务器认证 Token 的密钥和算法相同。换句话说,业务服务器同样可以创建有效的 Token。

如果业务服务器不能被信任,该怎么办?

不受信的业务服务器

遇到不受信的业务服务器时,很容易想到的办法是使用不同的密钥。认证服务器使用密钥1签发,业务服务器使用密钥2验证——这是典型非对称加密签名的应用场景。认证服务器自己使用私钥对 Token 签名,公开公钥。信任这个认证服务器的业务服务器保存公钥,用于验证签名。幸好,JWT 不仅可以使用 HMAC 签名,也可以使用 RSA(一种非对称加密算法)签名。

不过,当业务服务器已经不受信任的时候,多个业务服务器之间使用相同的 Token 对用户来说是不安全的。因为任何一个服务器拿到 Token 都可以仿冒用户去另一个服务器处理业务……悲剧随时可能发生。

为了防止这种情况发生,就需要在认证服务器产生 Token 的时候,把使用该 Token 的业务服务器的信息记录在 Token 中,这样当另一个业务服务器拿到这个 Token 的时候,发现它并不是自己应该验证的 Token,就可以直接拒绝。

现在,认证服务器不信任业务服务器,业务服务器相互也不信任,但前端是信任这些服务器的——如果前端不信任,就不会拿 Token 去请求验证。那么为什么会信任?可能是因为这些是同一家公司或者同一个项目中提供的若干服务构成的服务体系。

但是,前端信任不代表用户信任。如果 Token 不没有携带用户隐私(比如姓名),那么用户不会关心信任问题。但如果 Token 含有用户隐私的时候,用户得关心信任问题了。这时候认证服务就不得不再啰嗦一些,当用户请求 Token 的时候,问上一句,你真的要授权给某某某业务服务吗?而这个“某某某”,用户怎么知道它是不是真的“某某某”呢?用户当然不知道,甚至认证服务也不知道,因为公钥已经公开了,任何一个业务都可以声明自己是“某某某”。

为了得到用户的信任,认证服务就不得不帮助用户来甄别业务服务。所以,认证服器决定不公开公钥,而是要求业务服务先申请注册并通过审核。只有通过审核的业务服务器才能得到认证服务为它创建的,仅供它使用的公钥。如果该业务服务泄漏公钥带来风险,由该业务服务自行承担。现在认证服务可以清楚的告诉用户,“某某某”服务是什么了。如果用户还是不够信任,认证服务甚至可以问,某某某业务服务需要请求 A、B、C 三项个人数据,其中 A 是必须的,不然它不工作,是否允许授权?如果你授权,我就把你授权的几项数据加密放在 Token 中……

废话了这么多,有没有似曾相识……对了,这类似开放式 API 的认证过程。开发式 API 多采用 OAuth 认证,而关于 OAuth 的探讨资源非常丰富,这里就不深究了。

查看原文

笨兔儿 评论了文章 · 2018-11-20

Token 认证的来龙去脉

不久前,我在在前后端分离实践中提到了基于 Token 的认证,现在我们稍稍深入一些。

通常情况下,我们在讨论某个技术的时候,都是从问题开始。那么第一个问题:

为什么要用 Token?

而要回答这个问题很简单——因为它能解决问题!

可以解决哪些问题呢?

  1. Token 完全由应用管理,所以它可以避开同源策略
  2. Token 可以避免 CSRF 攻击
  3. Token 可以是无状态的,可以在多个服务间共享

Token 是在服务端产生的。如果前端使用用户名/密码向服务端请求认证,服务端认证成功,那么在服务端会返回 Token 给前端。前端可以在每次请求的时候带上 Token 证明自己的合法地位。如果这个 Token 在服务端持久化(比如存入数据库),那它就是一个永久的身份令牌。

于是,又一个问题产生了:需要为 Token 设置有效期吗?

需要设置有效期吗?

对于这个问题,我们不妨先看两个例子。一个例子是登录密码,一般要求定期改变密码,以防止泄漏,所以密码是有有效期的;另一个例子是安全证书。SSL 安全证书都有有效期,目的是为了解决吊销的问题,对于这个问题的详细情况,来看看知乎的回答。所以无论是从安全的角度考虑,还是从吊销的角度考虑,Token 都需要设有效期。

那么有效期多长合适呢?

只能说,根据系统的安全需要,尽可能的短,但也不能短得离谱——想像一下手机的自动熄屏时间,如果设置为 10 秒钟无操作自动熄屏,再次点亮需要输入密码,会不会疯?如果你觉得不会,那就亲自试一试,设置成可以设置的最短时间,坚持一周就好(不排除有人适应这个时间,毕竟手机厂商也是有用户体验研究的)。

然后新问题产生了,如果用户在正常操作的过程中,Token 过期失效了,要求用户重新登录……用户体验岂不是很糟糕?

为了解决在操作过程不能让用户感到 Token 失效这个问题,有一种方案是在服务器端保存 Token 状态,用户每次操作都会自动刷新(推迟) Token 的过期时间——Session 就是采用这种策略来保持用户登录状态的。然而仍然存在这样一个问题,在前后端分离、单页 App 这些情况下,每秒种可能发起很多次请求,每次都去刷新过期时间会产生非常大的代价。如果 Token 的过期时间被持久化到数据库或文件,代价就更大了。所以通常为了提升效率,减少消耗,会把 Token 的过期时保存在缓存或者内存中。

还有另一种方案,使用 Refresh Token,它可以避免频繁的读写操作。这种方案中,服务端不需要刷新 Token 的过期时间,一旦 Token 过期,就反馈给前端,前端使用 Refresh Token 申请一个全新 Token 继续使用。这种方案中,服务端只需要在客户端请求更新 Token 的时候对 Refresh Token 的有效性进行一次检查,大大减少了更新有效期的操作,也就避免了频繁读写。当然 Refresh Token 也是有有效期的,但是这个有效期就可以长一点了,比如,以天为单位的时间。

时序图表示

使用 Token 和 Refresh Token 的时序图如下:

1)登录

clipboard.png

2)业务请求

clipboard.png

3)Token 过期,刷新 Token

clipboard.png

上面的时序图中并未提到 Refresh Token 过期怎么办。不过很显然,Refresh Token 既然已经过期,就该要求用户重新登录了。

当然还可以把这个机制设计得更复杂一些,比如,Refresh Token 每次使用的时候,都更新它的过期时间,直到与它的创建时间相比,已经超过了非常长的一段时间(比如三个月),这等于是在相当长一段时间内允许 Refresh Token 自动续期。

到目前为止,Token 都是有状态的,即在服务端需要保存并记录相关属性。那说好的无状态呢,怎么实现?

无状态 Token

如果我们把所有状态信息都附加在 Token 上,服务器就可以不保存。但是服务端仍然需要认证 Token 有效。不过只要服务端能确认是自己签发的 Token,而且其信息未被改动过,那就可以认为 Token 有效——“签名”可以作此保证。平时常说的签名都存在一方签发,另一方验证的情况,所以要使用非对称加密算法。但是在这里,签发和验证都是同一方,所以对称加密算法就能达到要求,而对称算法比非对称算法要快得多(可达数十倍差距)。更进一步思考,对称加密算法除了加密,还带有还原加密内容的功能,而这一功能在对 Token 签名时并无必要——既然不需要解密,摘要(散列)算法就会更快。可以指定密码的散列算法,自然是 HMAC。

上面说了这么多,还需要自己去实现吗?不用!JWT 已经定义了详细的规范,而且有各种语言的若干实现。

不过在使用无状态 Token 的时候在服务端会有一些变化,服务端虽然不保存有效的 Token 了,却需要保存未到期却已注销的 Token。如果一个 Token 未到期就被用户主动注销,那么服务器需要保存这个被注销的 Token,以便下次收到使用这个仍在有效期内的 Token 时判其无效。有没有感到一点沮丧?

在前端可控的情况下(比如前端和服务端在同一个项目组内),可以协商:前端一但注销成功,就丢掉本地保存(比如保存在内存、LocalStorage 等)的 Token 和 Refresh Token。基于这样的约定,服务器就可以假设收到的 Token 一定是没注销的(因为注销之后前端就不会再使用了)。

如果前端不可控的情况,仍然可以进行上面的假设,但是这种情况下,需要尽量缩短 Token 的有效期,而且必须在用户主动注销的情况下让 Refresh Token 无效。这个操作存在一定的安全漏洞,因为用户会认为已经注销了,实际上在较短的一段时间内并没有注销。如果应用设计中,这点漏洞并不会造成什么损失,那采用这种策略就是可行的。

在使用无状态 Token 的时候,有两点需要注意:

  1. Refresh Token 有效时间较长,所以它应该在服务器端有状态,以增强安全性,确保用户注销时可控
  2. 应该考虑使用二次认证来增强敏感操作的安全性

到此,关于 Token 的话题似乎差不多了——然而并没有,上面说的只是认证服务和业务服务集成在一起的情况,如果是分离的情况呢?

分离认证服务

当 Token 无状态之后,单点登录就变得容易了。前端拿到一个有效的 Token,它就可以在任何同一体系的服务上认证通过——只要它们使用同样的密钥和算法来认证 Token 的有效性。就样这样:

clipboard.png

当然,如果 Token 过期了,前端仍然需要去认证服务更新 Token:

clipboard.png

可见,虽然认证和业务分离了,实际即并没产生多大的差异。当然,这是建立在认证服务器信任业务服务器的前提下,因为认证服务器产生 Token 的密钥和业务服务器认证 Token 的密钥和算法相同。换句话说,业务服务器同样可以创建有效的 Token。

如果业务服务器不能被信任,该怎么办?

不受信的业务服务器

遇到不受信的业务服务器时,很容易想到的办法是使用不同的密钥。认证服务器使用密钥1签发,业务服务器使用密钥2验证——这是典型非对称加密签名的应用场景。认证服务器自己使用私钥对 Token 签名,公开公钥。信任这个认证服务器的业务服务器保存公钥,用于验证签名。幸好,JWT 不仅可以使用 HMAC 签名,也可以使用 RSA(一种非对称加密算法)签名。

不过,当业务服务器已经不受信任的时候,多个业务服务器之间使用相同的 Token 对用户来说是不安全的。因为任何一个服务器拿到 Token 都可以仿冒用户去另一个服务器处理业务……悲剧随时可能发生。

为了防止这种情况发生,就需要在认证服务器产生 Token 的时候,把使用该 Token 的业务服务器的信息记录在 Token 中,这样当另一个业务服务器拿到这个 Token 的时候,发现它并不是自己应该验证的 Token,就可以直接拒绝。

现在,认证服务器不信任业务服务器,业务服务器相互也不信任,但前端是信任这些服务器的——如果前端不信任,就不会拿 Token 去请求验证。那么为什么会信任?可能是因为这些是同一家公司或者同一个项目中提供的若干服务构成的服务体系。

但是,前端信任不代表用户信任。如果 Token 不没有携带用户隐私(比如姓名),那么用户不会关心信任问题。但如果 Token 含有用户隐私的时候,用户得关心信任问题了。这时候认证服务就不得不再啰嗦一些,当用户请求 Token 的时候,问上一句,你真的要授权给某某某业务服务吗?而这个“某某某”,用户怎么知道它是不是真的“某某某”呢?用户当然不知道,甚至认证服务也不知道,因为公钥已经公开了,任何一个业务都可以声明自己是“某某某”。

为了得到用户的信任,认证服务就不得不帮助用户来甄别业务服务。所以,认证服器决定不公开公钥,而是要求业务服务先申请注册并通过审核。只有通过审核的业务服务器才能得到认证服务为它创建的,仅供它使用的公钥。如果该业务服务泄漏公钥带来风险,由该业务服务自行承担。现在认证服务可以清楚的告诉用户,“某某某”服务是什么了。如果用户还是不够信任,认证服务甚至可以问,某某某业务服务需要请求 A、B、C 三项个人数据,其中 A 是必须的,不然它不工作,是否允许授权?如果你授权,我就把你授权的几项数据加密放在 Token 中……

废话了这么多,有没有似曾相识……对了,这类似开放式 API 的认证过程。开发式 API 多采用 OAuth 认证,而关于 OAuth 的探讨资源非常丰富,这里就不深究了。

查看原文

笨兔儿 评论了文章 · 2018-11-20

Token 认证的来龙去脉

不久前,我在在前后端分离实践中提到了基于 Token 的认证,现在我们稍稍深入一些。

通常情况下,我们在讨论某个技术的时候,都是从问题开始。那么第一个问题:

为什么要用 Token?

而要回答这个问题很简单——因为它能解决问题!

可以解决哪些问题呢?

  1. Token 完全由应用管理,所以它可以避开同源策略
  2. Token 可以避免 CSRF 攻击
  3. Token 可以是无状态的,可以在多个服务间共享

Token 是在服务端产生的。如果前端使用用户名/密码向服务端请求认证,服务端认证成功,那么在服务端会返回 Token 给前端。前端可以在每次请求的时候带上 Token 证明自己的合法地位。如果这个 Token 在服务端持久化(比如存入数据库),那它就是一个永久的身份令牌。

于是,又一个问题产生了:需要为 Token 设置有效期吗?

需要设置有效期吗?

对于这个问题,我们不妨先看两个例子。一个例子是登录密码,一般要求定期改变密码,以防止泄漏,所以密码是有有效期的;另一个例子是安全证书。SSL 安全证书都有有效期,目的是为了解决吊销的问题,对于这个问题的详细情况,来看看知乎的回答。所以无论是从安全的角度考虑,还是从吊销的角度考虑,Token 都需要设有效期。

那么有效期多长合适呢?

只能说,根据系统的安全需要,尽可能的短,但也不能短得离谱——想像一下手机的自动熄屏时间,如果设置为 10 秒钟无操作自动熄屏,再次点亮需要输入密码,会不会疯?如果你觉得不会,那就亲自试一试,设置成可以设置的最短时间,坚持一周就好(不排除有人适应这个时间,毕竟手机厂商也是有用户体验研究的)。

然后新问题产生了,如果用户在正常操作的过程中,Token 过期失效了,要求用户重新登录……用户体验岂不是很糟糕?

为了解决在操作过程不能让用户感到 Token 失效这个问题,有一种方案是在服务器端保存 Token 状态,用户每次操作都会自动刷新(推迟) Token 的过期时间——Session 就是采用这种策略来保持用户登录状态的。然而仍然存在这样一个问题,在前后端分离、单页 App 这些情况下,每秒种可能发起很多次请求,每次都去刷新过期时间会产生非常大的代价。如果 Token 的过期时间被持久化到数据库或文件,代价就更大了。所以通常为了提升效率,减少消耗,会把 Token 的过期时保存在缓存或者内存中。

还有另一种方案,使用 Refresh Token,它可以避免频繁的读写操作。这种方案中,服务端不需要刷新 Token 的过期时间,一旦 Token 过期,就反馈给前端,前端使用 Refresh Token 申请一个全新 Token 继续使用。这种方案中,服务端只需要在客户端请求更新 Token 的时候对 Refresh Token 的有效性进行一次检查,大大减少了更新有效期的操作,也就避免了频繁读写。当然 Refresh Token 也是有有效期的,但是这个有效期就可以长一点了,比如,以天为单位的时间。

时序图表示

使用 Token 和 Refresh Token 的时序图如下:

1)登录

clipboard.png

2)业务请求

clipboard.png

3)Token 过期,刷新 Token

clipboard.png

上面的时序图中并未提到 Refresh Token 过期怎么办。不过很显然,Refresh Token 既然已经过期,就该要求用户重新登录了。

当然还可以把这个机制设计得更复杂一些,比如,Refresh Token 每次使用的时候,都更新它的过期时间,直到与它的创建时间相比,已经超过了非常长的一段时间(比如三个月),这等于是在相当长一段时间内允许 Refresh Token 自动续期。

到目前为止,Token 都是有状态的,即在服务端需要保存并记录相关属性。那说好的无状态呢,怎么实现?

无状态 Token

如果我们把所有状态信息都附加在 Token 上,服务器就可以不保存。但是服务端仍然需要认证 Token 有效。不过只要服务端能确认是自己签发的 Token,而且其信息未被改动过,那就可以认为 Token 有效——“签名”可以作此保证。平时常说的签名都存在一方签发,另一方验证的情况,所以要使用非对称加密算法。但是在这里,签发和验证都是同一方,所以对称加密算法就能达到要求,而对称算法比非对称算法要快得多(可达数十倍差距)。更进一步思考,对称加密算法除了加密,还带有还原加密内容的功能,而这一功能在对 Token 签名时并无必要——既然不需要解密,摘要(散列)算法就会更快。可以指定密码的散列算法,自然是 HMAC。

上面说了这么多,还需要自己去实现吗?不用!JWT 已经定义了详细的规范,而且有各种语言的若干实现。

不过在使用无状态 Token 的时候在服务端会有一些变化,服务端虽然不保存有效的 Token 了,却需要保存未到期却已注销的 Token。如果一个 Token 未到期就被用户主动注销,那么服务器需要保存这个被注销的 Token,以便下次收到使用这个仍在有效期内的 Token 时判其无效。有没有感到一点沮丧?

在前端可控的情况下(比如前端和服务端在同一个项目组内),可以协商:前端一但注销成功,就丢掉本地保存(比如保存在内存、LocalStorage 等)的 Token 和 Refresh Token。基于这样的约定,服务器就可以假设收到的 Token 一定是没注销的(因为注销之后前端就不会再使用了)。

如果前端不可控的情况,仍然可以进行上面的假设,但是这种情况下,需要尽量缩短 Token 的有效期,而且必须在用户主动注销的情况下让 Refresh Token 无效。这个操作存在一定的安全漏洞,因为用户会认为已经注销了,实际上在较短的一段时间内并没有注销。如果应用设计中,这点漏洞并不会造成什么损失,那采用这种策略就是可行的。

在使用无状态 Token 的时候,有两点需要注意:

  1. Refresh Token 有效时间较长,所以它应该在服务器端有状态,以增强安全性,确保用户注销时可控
  2. 应该考虑使用二次认证来增强敏感操作的安全性

到此,关于 Token 的话题似乎差不多了——然而并没有,上面说的只是认证服务和业务服务集成在一起的情况,如果是分离的情况呢?

分离认证服务

当 Token 无状态之后,单点登录就变得容易了。前端拿到一个有效的 Token,它就可以在任何同一体系的服务上认证通过——只要它们使用同样的密钥和算法来认证 Token 的有效性。就样这样:

clipboard.png

当然,如果 Token 过期了,前端仍然需要去认证服务更新 Token:

clipboard.png

可见,虽然认证和业务分离了,实际即并没产生多大的差异。当然,这是建立在认证服务器信任业务服务器的前提下,因为认证服务器产生 Token 的密钥和业务服务器认证 Token 的密钥和算法相同。换句话说,业务服务器同样可以创建有效的 Token。

如果业务服务器不能被信任,该怎么办?

不受信的业务服务器

遇到不受信的业务服务器时,很容易想到的办法是使用不同的密钥。认证服务器使用密钥1签发,业务服务器使用密钥2验证——这是典型非对称加密签名的应用场景。认证服务器自己使用私钥对 Token 签名,公开公钥。信任这个认证服务器的业务服务器保存公钥,用于验证签名。幸好,JWT 不仅可以使用 HMAC 签名,也可以使用 RSA(一种非对称加密算法)签名。

不过,当业务服务器已经不受信任的时候,多个业务服务器之间使用相同的 Token 对用户来说是不安全的。因为任何一个服务器拿到 Token 都可以仿冒用户去另一个服务器处理业务……悲剧随时可能发生。

为了防止这种情况发生,就需要在认证服务器产生 Token 的时候,把使用该 Token 的业务服务器的信息记录在 Token 中,这样当另一个业务服务器拿到这个 Token 的时候,发现它并不是自己应该验证的 Token,就可以直接拒绝。

现在,认证服务器不信任业务服务器,业务服务器相互也不信任,但前端是信任这些服务器的——如果前端不信任,就不会拿 Token 去请求验证。那么为什么会信任?可能是因为这些是同一家公司或者同一个项目中提供的若干服务构成的服务体系。

但是,前端信任不代表用户信任。如果 Token 不没有携带用户隐私(比如姓名),那么用户不会关心信任问题。但如果 Token 含有用户隐私的时候,用户得关心信任问题了。这时候认证服务就不得不再啰嗦一些,当用户请求 Token 的时候,问上一句,你真的要授权给某某某业务服务吗?而这个“某某某”,用户怎么知道它是不是真的“某某某”呢?用户当然不知道,甚至认证服务也不知道,因为公钥已经公开了,任何一个业务都可以声明自己是“某某某”。

为了得到用户的信任,认证服务就不得不帮助用户来甄别业务服务。所以,认证服器决定不公开公钥,而是要求业务服务先申请注册并通过审核。只有通过审核的业务服务器才能得到认证服务为它创建的,仅供它使用的公钥。如果该业务服务泄漏公钥带来风险,由该业务服务自行承担。现在认证服务可以清楚的告诉用户,“某某某”服务是什么了。如果用户还是不够信任,认证服务甚至可以问,某某某业务服务需要请求 A、B、C 三项个人数据,其中 A 是必须的,不然它不工作,是否允许授权?如果你授权,我就把你授权的几项数据加密放在 Token 中……

废话了这么多,有没有似曾相识……对了,这类似开放式 API 的认证过程。开发式 API 多采用 OAuth 认证,而关于 OAuth 的探讨资源非常丰富,这里就不深究了。

查看原文

笨兔儿 发布了文章 · 2018-11-20

hadoop kerberos操作

Hadoop Kerberos

提交YARN任务失败

diagnostics: Application application_1542706493784_0005 failed 2 times due to AM Container for appattempt_1542706493784_0005_000002 exited with  exitCode: -1000
Failing this attempt.Diagnostics: [2018-11-20 17:48:05.088]Application application_1542706493784_0005 initialization failed (exitCode=255) with output: main : command provided 0
main : run as user is xxxx
main : requested yarn user is xxxx
User xxxx not found
  • 解决方案
因为在hadoop集群中没有创建xxx用户
查看原文

赞 0 收藏 0 评论 0

笨兔儿 收藏了文章 · 2018-11-13

[译]ElasticSearch数据类型--string类型已死, 字符串数据永生

原文链接: https://www.elastic.co/blog/s...

Text vs. keyword

随着ElasticSearch 5.0的到来, 同时也迎来了该版本的重大特性之一: 移除了string类型. 这个变动的根本原因是string类型会给我们带来很多困惑: 因为ElasticSearch对字符串拥有两种完全不同的搜索方式. 你可以按照整个文本进行匹配, 即关键词搜索(keyword search), 也可以按单个字符匹配, 即全文搜索(full-text search). 对ElasticSearch稍有了解的人都知道, 前者的字符串被称为not-analyzed字符, 而后者被称作analyzed字符串.

事实上, 同一种类型用于应对两种不同的使用场景是会让人崩溃的, 因为有些选项只对其一的场景设置有效.例如position_increment_gapnot-analyzed字符就不会起作用, 而像ignore_above对于analyzed字符串就很难区分它到底是对整个字符串的值有效还是对单独的每个分词有效(在这种场景, ignore_above确实只对整个字符串值有效, 而对单个分词的限制可以使用limit设置).

为了避免上述尴尬, string字段被拆分成两种新的数据类型: text用于全文搜索的, 而keyword用于关键词搜索.

新的默认类型

做了这个类型分解之后, 我们对string字段的默认dynamic mappings 也做了改变. 在以前刚接接触ElasticSearch时, 如果需要对某个字段的所有取值做聚合, 你不得不对这些数据重做索引. 假如你正在处理的文档中包含一个city字段. 对这个字段做聚合的话会分别给出newyork的总数, 而非我们通常期望的New York的总数.让人沮丧的是为了达到我们希望的结果, 我们必须对这个字段重新进行索引.

为了不让事情变得这么糟糕, ElasticSearch决定从Logstash中借取思路: 字符串将默认被同时映射成textkeyword类型. 例如对下面的文档进行索引后:

{
    "foo": "bar"
}

ElasticSearch将会为你创建下面的动态映射(dynamic mappings):

{
    "foo": {
        "type": "text",
        "fields": {
            "keyword": {
                "type": "keyword",
                "ignore_above": 256
            }
        }
    }
}

当然, 基于这个映射你即可以在foo字段上进行全文搜索, 也可以通过foo.keyword字段实现关键词搜索及数据聚合.

禁用这个特性也很方便: 你只需要在定义mapping时显式声明字符串字段的类型或者使用一个动态模板(dynamic template)来匹配你所有的字符串字段即可. 例如通过下面的dynamic template就可以恢复到在ElasticSearch 2.x中使用的dynamic template的效果:

{
    "match_mapping_type": "string",
    "mapping": {
        "type": "text"
    }
}

如何迁移到新版本

通常, 迁移工作是非常容易的. 以前映射到analyzed的字符串的字段:

{
    "foo": {
        "type": "string",
        "index": "analyzed"
    }
}

如今只要映射为text即可:

{
    "foo": {
        "type": "text",
        "index": true
    }
}

以前被定义为not_analyzed的字符串字段:

{
    "foo": {
        "type": "string",
        "index": "not_analyzed"
    }
}

也只需要被定义为keyword即可:

{
    "foo": {
        "type": "keyword",
        "index": true
    }
}

如上所述, string字段被重新定义为textkeyword字段. 对于上面的index属性, 因为在新的定义中我们不需要三种状态(在以前的string定义中可以是analyzed, not_analyzedno), 所以只简单的定义成了boolean值, 以告知ElasticSearch是否可在该字段上进行搜索.

向后兼容

因为大的版本升级本身就充满挑战, 因此我们尽力不让在在升级ElasticSearch过程中更新你的mapping字义. 首先, string字段可以继续在已定义的2.x版本的索引中继续使用, 而当创建新的索引时, ElasticSearch会做些处理以自动把string映射成等价的textkeyword. 如果在你已有的索引模板(index template)中定义有string字段, 这一点将非常有用, 因为这些模板无须改动即可使用到ElasticSearch 5.x中. 话说回来, 你还是需要着手做这些模板做些升级, 因为在ElasticSearch 6.0中我们可能会移除这个向后兼容的逻辑.

查看原文

笨兔儿 发布了文章 · 2018-11-01

CDH 启用Kerberos安全认证

KDC服务器搭建

Cloudera Manager服务器上安装KDC服务

  • 安装KDC服务
yum -y install krb5-server krb5-libs krb5-auth-dialog krb5-workstation
  • 集群其他服务器安装客户端服务
yum -y install krb5-auth-dialog krb5-workstation

Kerberos 错误列表

kinit时错误信息 kinit: Cannot find KDC for realm "XXXX.CN" while getting initial credentials

看错误信息提示是你没有定义realm,这时需要检查/var/kerberos/krb5kdc/kdc.conf/etc/krb5kdc.conf两文件是否有定义

查看原文

赞 0 收藏 0 评论 0

笨兔儿 发布了文章 · 2018-10-19

阿里云运维总结

阿里云运维总结

OSS图片规则

图片规则添加模糊处理

q_90/blur,r_30,s_30
q_90 压缩率
/blur,r_30,s_30 图片模糊比率 数字越小越模糊
查看原文

赞 0 收藏 0 评论 0

笨兔儿 赞了文章 · 2018-10-19

[译]ElasticSearch数据类型--string类型已死, 字符串数据永生

原文链接: https://www.elastic.co/blog/s...

Text vs. keyword

随着ElasticSearch 5.0的到来, 同时也迎来了该版本的重大特性之一: 移除了string类型. 这个变动的根本原因是string类型会给我们带来很多困惑: 因为ElasticSearch对字符串拥有两种完全不同的搜索方式. 你可以按照整个文本进行匹配, 即关键词搜索(keyword search), 也可以按单个字符匹配, 即全文搜索(full-text search). 对ElasticSearch稍有了解的人都知道, 前者的字符串被称为not-analyzed字符, 而后者被称作analyzed字符串.

事实上, 同一种类型用于应对两种不同的使用场景是会让人崩溃的, 因为有些选项只对其一的场景设置有效.例如position_increment_gapnot-analyzed字符就不会起作用, 而像ignore_above对于analyzed字符串就很难区分它到底是对整个字符串的值有效还是对单独的每个分词有效(在这种场景, ignore_above确实只对整个字符串值有效, 而对单个分词的限制可以使用limit设置).

为了避免上述尴尬, string字段被拆分成两种新的数据类型: text用于全文搜索的, 而keyword用于关键词搜索.

新的默认类型

做了这个类型分解之后, 我们对string字段的默认dynamic mappings 也做了改变. 在以前刚接接触ElasticSearch时, 如果需要对某个字段的所有取值做聚合, 你不得不对这些数据重做索引. 假如你正在处理的文档中包含一个city字段. 对这个字段做聚合的话会分别给出newyork的总数, 而非我们通常期望的New York的总数.让人沮丧的是为了达到我们希望的结果, 我们必须对这个字段重新进行索引.

为了不让事情变得这么糟糕, ElasticSearch决定从Logstash中借取思路: 字符串将默认被同时映射成textkeyword类型. 例如对下面的文档进行索引后:

{
    "foo": "bar"
}

ElasticSearch将会为你创建下面的动态映射(dynamic mappings):

{
    "foo": {
        "type": "text",
        "fields": {
            "keyword": {
                "type": "keyword",
                "ignore_above": 256
            }
        }
    }
}

当然, 基于这个映射你即可以在foo字段上进行全文搜索, 也可以通过foo.keyword字段实现关键词搜索及数据聚合.

禁用这个特性也很方便: 你只需要在定义mapping时显式声明字符串字段的类型或者使用一个动态模板(dynamic template)来匹配你所有的字符串字段即可. 例如通过下面的dynamic template就可以恢复到在ElasticSearch 2.x中使用的dynamic template的效果:

{
    "match_mapping_type": "string",
    "mapping": {
        "type": "text"
    }
}

如何迁移到新版本

通常, 迁移工作是非常容易的. 以前映射到analyzed的字符串的字段:

{
    "foo": {
        "type": "string",
        "index": "analyzed"
    }
}

如今只要映射为text即可:

{
    "foo": {
        "type": "text",
        "index": true
    }
}

以前被定义为not_analyzed的字符串字段:

{
    "foo": {
        "type": "string",
        "index": "not_analyzed"
    }
}

也只需要被定义为keyword即可:

{
    "foo": {
        "type": "keyword",
        "index": true
    }
}

如上所述, string字段被重新定义为textkeyword字段. 对于上面的index属性, 因为在新的定义中我们不需要三种状态(在以前的string定义中可以是analyzed, not_analyzedno), 所以只简单的定义成了boolean值, 以告知ElasticSearch是否可在该字段上进行搜索.

向后兼容

因为大的版本升级本身就充满挑战, 因此我们尽力不让在在升级ElasticSearch过程中更新你的mapping字义. 首先, string字段可以继续在已定义的2.x版本的索引中继续使用, 而当创建新的索引时, ElasticSearch会做些处理以自动把string映射成等价的textkeyword. 如果在你已有的索引模板(index template)中定义有string字段, 这一点将非常有用, 因为这些模板无须改动即可使用到ElasticSearch 5.x中. 话说回来, 你还是需要着手做这些模板做些升级, 因为在ElasticSearch 6.0中我们可能会移除这个向后兼容的逻辑.

查看原文

赞 10 收藏 7 评论 1

笨兔儿 发布了文章 · 2018-10-12

Spark2.3.1+Kafka0.9使用Direct模式消费信息异常

Spark2.3.1+Kafka使用Direct模式消费信息

Maven依赖

<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-streaming-kafka-0-8_2.11</artifactId>
    <version>2.3.1</version>
</dependency>
<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-streaming_2.11</artifactId>
    <version>2.3.1</version>
</dependency>

2.3.1spark版本

Direct模式代码

import kafka.serializer.StringDecoder
import org.apache.spark.streaming.kafka.KafkaUtils
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.{SparkConf, SparkContext}

object Test {

  val zkQuorum = "mirrors.mucang.cn:2181"
  val groupId = "nginx-cg"
  val topic = Map("nginx-log" -> 1)

  val KAFKA_INTERVAL = 10

  case class NginxInof(domain: String, ip: String)

  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setAppName("NginxLogAnalyze").setMaster("local[*]")
    val sparkContext = new SparkContext(sparkConf)

    val streamContext = new StreamingContext(sparkContext, Seconds(KAFKA_INTERVAL))

    val kafkaParam = Map[String, String](
      "bootstrap.servers" -> "xx.xx.cn:9092",
      "group.id" -> "nginx-cg",
      "auto.offset.reset" -> "largest"
    )

    val topic = Set("nginx-log")

    val kafkaStream = KafkaUtils.createDirectStream(streamContext, kafkaParam, topic)

    val counter = kafkaStream
      .map(_.toString().split(" "))
      .map(item => (item(0).split(",")(1) + "-" + item(2), 1))
      .reduceByKey((x, y) => (x + y))

    counter.foreachRDD(rdd => {
      rdd.foreach(println)
    })


    streamContext.start()
    streamContext.awaitTermination()

  }

}

largest 因为kafka版本过低不支持latest

异常信息

Caused by: java.lang.NoSuchMethodException: scala.runtime.Nothing$.<init>(kafka.utils.VerifiableProperties)
    at java.lang.Class.getConstructor0(Class.java:3082)
    at java.lang.Class.getConstructor(Class.java:1825)
    at org.apache.spark.streaming.kafka.KafkaRDD$KafkaRDDIterator.<init>(KafkaRDD.scala:153)
    at org.apache.spark.streaming.kafka.KafkaRDD.compute(KafkaRDD.scala:136)
    at org.apache.spark.rdd.RDD.computeOrReadCheckpoint(RDD.scala:324)
    at org.apache.spark.rdd.RDD.iterator(RDD.scala:288)
    at org.apache.spark.rdd.MapPartitionsRDD.compute(MapPartitionsRDD.scala:38)
    at org.apache.spark.rdd.RDD.computeOrReadCheckpoint(RDD.scala:324)
    at org.apache.spark.rdd.RDD.iterator(RDD.scala:288)
    at org.apache.spark.rdd.MapPartitionsRDD.compute(MapPartitionsRDD.scala:38)
    at org.apache.spark.rdd.RDD.computeOrReadCheckpoint(RDD.scala:324)
    at org.apache.spark.rdd.RDD.iterator(RDD.scala:288)
    at org.apache.spark.scheduler.ShuffleMapTask.runTask(ShuffleMapTask.scala:96)
    at org.apache.spark.scheduler.ShuffleMapTask.runTask(ShuffleMapTask.scala:53)
    at org.apache.spark.scheduler.Task.run(Task.scala:109)
    at org.apache.spark.executor.Executor$TaskRunner.run(Executor.scala:345)
    ... 3 more

解决方案

在验证kafka属性时不能使用scala默认的类,需要指定kafka带的类
createDirectStream[String, String, StringDecoder, StringDecoder]其中StringDecoder必须是kafka.serializer.StringDecoder

查看原文

赞 0 收藏 0 评论 0

笨兔儿 发布了文章 · 2018-10-10

运维小事件-持续更新

运维小事件

Linux系统

系统随机数

# 两种获取随机值的伪设备
/dev/random /dev/urandom
cat /dev/urandom |od -x | tr -d  ' '| head -n 1

random 通过进程中断获取随机数,如果中断次数不够会阻塞程序,而urandom是不会阻塞程序,所以random的精度高urandom低些还可能会有重复 
查看原文

赞 0 收藏 0 评论 0