花钱的年华

江南白衣,公众号:春天的旁边

服务化体系之-兼容性与版本号

| Filed under 技术

准备挽起袖子写个服务化的大系列,不想错过的同学,先把公众号关注了,jnby1978。

家大业大之后,服务的版本和兼容性就是个让人不得不正视的问题。

最近,路上有个说法,既然都是微服务了,那不同的版本可以认为是两个完全不一样的微服务,没必要再保留版本号了。

这篇文章按着唯品会的实战经历来探讨一下。

我们平时面对的现状是:服务每周每天都在升级,有些升级是业务逻辑升级,接口不变;有些是接口变了,但是兼容的,偶然还有一些是不兼容的。

家大业大的体系永远不可能在服务接口升级的时候,同时(比如同一个深夜,同一个小时甚至同一分钟)将所有调用者的客户端也升级的,相反所有客户端的升级可能是很长一段时间的事情,而且可能客户端还没全升完,服务端的接口又变了。

 

1. 兼容性原则

先搞些铺垫,我们一般认为下面的情况是兼容的:

  1. 增加新方法
  2. 增加可选的参数
  3. 修改参数为可选
  4. 删除参数
  5. 框架支持的话,参数的名称,顺序也可以改(如Thrift)

参数是对象的话,其属性的变更参见2-5条。

然后一般认为下面的情况是不兼容的:

  1. 修改方法的名称
  2. 删除方法
  3. 增加必填的参数
  4. 修改参数为必填
  5. 修改参数和返回值的类型

参数是对象的话,属性的变更参照3-5条。

 

2. 不兼容时服务如何升级?

服务要进行不兼容的升级时,前面说了客户端不可能同时升级的,那怎么办?

一种是不修改原来的接口,直接在同一个服务里增加新的方法来解决。这种应该是最简单的做法,少量接口变动时优先使用,就是有点dirty。

一种是同时运行新旧两个服务。这种方式干净,但麻烦,要注意:

  • 要保证旧版的客户端SDK,调用不能被路由到新服务上。
  • 随着客户端不断割接过来,控制好新旧两个的集群容量。

 

3. 版本号原则

服务的版本号,和软件的版本号一样,一般整成三位:

第一位:不兼容的大版本, 如1.0 vs 2.0
第二位:兼容的新功能版本,如1.1 vs 1.2
第三位:兼容的BugFix版本,如1.1.0 vs 1.1.1

果拿着低版本的SDK(如1.0.0) 发起请求,会被服务化框架路由到所有的兼容版本上(如1.1.1,1.2.0),但不会到不兼容的版本上的(如2.0.1)。

 

4. 最终问题,到底要不要版本号?

4.1 版本号用于标示SDK版本是有益的

即使是兼容的小版本。

版本号能让服务端与客户端两头的开发人员更好的对话。毕竟所谓兼容,有时候也有着某种代价与折衷,互相明确彼此的版本会更好。

如果服务治理中心,能让所有服务提供者一目了然各个调用者的版本会更好,起码方便催人升级呀。

同时,在中央服务文档中心,有了版本号后也能为服务接口的每一个小版本保留一份文档。

 

不兼容的版本,怎么办?

像我们这种命名渣,好不容易为服务搞了个贴切的名字,再想第二个其实很不容易,一般只能在函数名,服务名里直接带上数字,如GoodsService2,或者GoodsServiceNew,GoodsNewSerivce.....

因此,如果加新方法的简单方式撑不住了,要独立新旧两个服务时,还是继续使用版本号来区分吧。当然,引入了版本号在框架里也引入了一定的复杂度,见后。
 

4.3 基于版本的配置

我们家框架之前的设计,将配置的粒度支持到每个小版本上,比如1.0版本的超时是10ms,1.1版本是20ms。然后根据客户端的版本来就近选择,回头来看,白白增加复杂度....

 

其实兼容的小版本只有一份配置就好,明年得简化。

 

但不兼容的大版本还是要两份,比如自定义的路由。不兼容的版本本质上真的就是两个微服务了。

 

4.4 最终结论

建议还是有版本, 服务化框架里的相关逻辑,只取第一位的主版本号。第二、三位则用于SDK版本管理沟通,以及中央服务文档中心。

当然,以上只是针对唯品会这种体量的公司的一家之言,各有各的玩法,并非四海皆准。

转载请保留链接:http://calvin1978.blogcn.com/articles/version.html

服务化体系之-限流

| Filed under 技术

(上)设计篇

在实现算法之前,先临时客串一下产品经理,尝试用最少的字,把“限流”这简单二字所展开的种种需求给说清楚。

 

1.各种目的

1. 保护每个服务节点。
2. 保护服务集群背后的资源,比如数据库。
3. 避免单个调用者过度使用服务,影响其他调用者。

 

2. 各种设定维度

2.1. 节点级别 vs 集群级别

如果以保护每个服务节点为目的,可以简单的在本地做节点级别的限流。

但如果以保护服务集群背后的资源为目的,就需要做集群级别的限流。

集群级别的一个做法是使用Redis, Memcached之类做一个集群级别的计数器。但额外多一次访问Redis的消耗,代价有点大。

而另一个做法是把集群限流总数分摊到每个节点上,但一是流量不是100%均匀时会不够精准,二是如果使用Docker动态缩扩容,需要动态更新这个分摊数。

 

2.2 客户端 vs 服务端

当以保护服务端的节点为目的,应在服务端设定,因为有多少调用者是未知的。

当以避免单个调用者过度使用服务为目的,可以针对客户端节点或客户端集群设定限流。此时限流可以在客户端实现,节约了网络往返;也可以在服务端实现,让所有限流逻辑集中于一处发生。

 

2.3 服务级别 vs 方法级别

可以对消耗特别大的方法专门配置,比如复杂的查询,昂贵的写操作。

然后其他方法使用统一的值,或者配一个所有方法加起来的总和。

 

3. 各种触发条件

触发条件的设定,难点在于服务的容量,受着本服务节点的能力,背后的资源的能力,下游服务的响应的多重约束。

3.1 静态配置固定值

当然,这个固定值可以被动态更新。

3.2 根据预设规则触发

规则的条件可以是服务平均时延,可以是背后数据库的CPU情况等。

比如平时不限流,当服务时延大于100ms,则触发限流500 QPS。

还可以是多级条件,比如>100ms 限流500 QPS, >200ms 限流200 QPS。

条件反馈由监控系统配合服务治理中心来完成,但有些指标如服务延时也可以由服务节点自行计算,如服务延时。

3.3 全动态自动增减调控

这个诱人的想法,永远存在于老板的心里。

 

4. 各种处理

4.1 立刻返回拒绝错误

由客户端进行降级处理。

4.2 进行短暂的等待

短暂等待,期待有容量空余,直到超时,依然是客户端降级。

4.3 触发服务降级,调用服务端的降级方法

服务端的降级方法,走服务端的简单路径与预设值,则代表了服务端这边的态度和逻辑。

客户端降级与服务端降级各有适用的场景,等下一篇《服务降级》 再详述。

 

(下)实现篇

开涛的《聊聊高并发系统之限流特技》 已讲得非常好,这里再简单补充两句。

另一篇《接口限流算法总结》 也非常好非常详细,差别只在于对令牌桶的突发量的描述上,我有我自己的理解。

1. 并发控制

并发控制本身就是一种最简单的限流,包括:

框架本身的连接/线程限制
各种数据库连接池,Http连接池
服务/方法级别的信号量计数器限制

 

2. 窗口流量控制

窗口流量控制有几种做法。

一种最简单的计数器,维护一个单位时间内的Counter,如判断单位时间已经过去,则将Counter重置零。开涛文章里利用guava cache也是一种做法。

但此做法有时被认为粒度太粗,没有把QPS平摊到一秒的各个毫秒里,同时也没有很好的处理单位时间的边界,比如在前一秒的最后一毫秒里和下一秒的第一毫秒都触发了最大的请求数,将目光移动一下,就看到在两毫秒内发生了两倍的TPS。

于是,另一个改进版的做法是把单位时间拆分,比如把1秒分成5个200毫秒宽的桶,然后以滑动窗口来计算限流,好像就能很大程度上解决上面的两个问题了。。。。滑动窗口的算法,统一到《熔断篇》再来讨论。

最后一组就是漏桶算法或令牌桶算法了,下面会详述。

另外,集群规模的计数器,基于Memcachd或Redis的实现,也见开涛的博客。

 

3. 令牌桶算法(Token Bucket )

  1. 随着时间流逝,系统会按速率 1/rate 的时间间隔(如果rate=100,则间隔是10ms)往桶里加入Token
  2. 如果桶满了(burst),则丢弃新加入的令牌
  3. 每个请求进来,都要消耗一个Token,如果桶空了,则丢弃请求或等待有新的令牌放入。

图片来自开涛的博客,非常形象,好像不需要更多解析

4. 漏桶算法(Leaky Bucket )

简单的想象有一个木桶,有新请求就是不断的倒水进来,然后桶底下有个洞,按照固定的速率把水漏走,如果水进来的速度比漏走的快,桶可能就会满了,然后就拒绝请求。

可见,两个算法的基本描述上,只是方向不一样,其他没什么不同的。所以WikiMedia里说, 两个算法实现可以一样,对于相同的参数得到的限流效果是一样的。

 

5. Guava版的令牌桶实现 -- RateLimiter

Guava已实现了一个性能非常好的RateLimiter,基本不需要我们再费心实现。但其中有一些实现的细节,需要我们留意:

1. 支持桶外预借的突发

突发,原本是指如果单位时间的前半段流量较少,桶里会积累一些令牌,然后支持来一波大的瞬时流量,将前面积累的令牌消耗掉。

但在RateLimiter的实现里,还多了个桶外预借(我自己给他的命名),就是即使桶里没有多少令牌,你也可以消耗一波大的,然后桶里面在时间段内都没有新令牌。比如桶的容量是5,桶里面现在只有1个令牌,如果你要拿5个令牌,也可以,清了桶里的一个令牌,再预借4个。然后再过800毫秒,桶里才会出现新令牌。

可见,Guava版的RateLimiter对突发的支持,比原版的两种算法都要大,你几乎随时都可以一次过消费burst个令牌,不管现在桶里有没有积累的令牌。

不过有个副作用,就是如果前面都没什么流量,桶里累积了5个令牌,则你其实可以一次过消费10个令牌。。。不过那么一下,超借完接下来还是固定速率的,直到还清了旧账,才可能再来那么一下。

2. 支持等待可用令牌与立刻返回两种接口

3. 单位时段是秒,这有点不太好用,不支持设定5分钟的单位。

4. 发令牌的计算粒度是MicroSeconds,也就是最多支持一百万的QPS。

 

6. 唯品会自家框架的实现

6.1 各种设定维度

1. 在每个服务节点上进行限流统计。

如果要保护后端的数据库资源,需要自己分摊一下集群总流量到每台服务器上(没有全docker化自动伸缩前,根据当前节点数量自动调整阀值的意义不大)

2. 支持不同的调用者集群(比如购物车服务与订单服务)不同的阀值,当然也支持剔除特定调用者后的总阀值。

3. 在方法级别上统计限流,可以对消耗特别大的特定方法设置另外的阀值,当然其他方法就用一个统一的值。

暂不支持按服务中所有方法的总流量来限流,因为除非所有方法的消耗很接近,否则意义不大。

6.2 各种触发条件

目前只支持静态配置的触发条件。

未来的服务治理中心,要从各种监控系统里采集数据,然后根据多级条件动态调整规则。又或者服务节点上先找些自己也能统计的指标如服务延时,先动态起来,就是有重复计算之嫌。

6.3 各种处理

目前只支持直接返回限流异常,由客户端来进行降级逻辑。

未来要支持调用在服务端的降级方法,由服务端提供降级逻辑的短路径。

6.4 计数器实现

限流只在服务端进行。不重新发明轮子,直接就是Guava的RateLimiter。

对于客户端的并发数限制,未来也可以做一下。

服务化框架-分布式Unique ID的生成方法一览

| Filed under 技术

分布式的Unique ID的用途如此广泛,从业务对象Id到服务化体系里分布式调用链的TraceId,本文总结了林林总总的各种生成算法。

对于UID的要求,一乐那篇《业务系统需要什么样的ID生成器》的提法很好: 唯一性,时间相关,粗略有序,可反解,可制造。

 

1. 发号器

我接触的最早的Unique ID,就是Oracle的Sequence。

特点是准连续的自增数字,为什么说是准连续?因为性能考虑,每个Client一次会领20个ID回去慢慢用,用完了再来拿。另一个Client过来,拿的就是另外20个ID了。

新浪微博里,Tim用Redis做相同的事情,Incr一下拿一批ID回去。如果有多个数据中心,那就拿高位的几个bit来区分。

只要舍得在总架构里增加额外Redis带来的复杂度,一个64bit的long就够表达了,而且不可能有重复ID。

批量是关键,否则每个ID都远程调用一次谁也吃不消。

 

2. UUID

2.1 概述

Universally Unique IDentifier(UUID),有着正儿八经的RFC规范,是一个128bit的数字,也可以表现为32个16进制的字符(每个字符0-F的字符代表4bit),中间用"-"分割。

  • 时间戳+UUID版本号: 分三段占16个字符(60bit+4bit),
  • Clock Sequence号与保留字段:占4个字符(13bit+3bit),
  • 节点标识:占12个字符(48bit),

比如:

f81d4fae-7dec-11d0-a765-00a0c91e6bf6

实际上,UUID一共有多种算法,能用于TraceId的是:

  • version1: 基于时间的算法
  • version4: 基于随机数的算法

2.2 version 4 基于随机数的算法

先说Version4,这是最暴力的做法,也是JDK里的算法,不管原来各个位的含义了,除了少数几个位必须按规范填,其余全部用随机数表达。

JDK里的实现,用 SecureRandom生成了16个随机的Byte,用2个long来存储。记得加-Djava.security.egd=file:/dev/./urandom,
详见SecureRandom的江湖偏方与真实效果

 

2.3 version 1 基于时间的算法

然后是Version1,严格守着原来各个位的规矩:

时间戳:有满满的60bit,所以可以尽情花,以100纳秒为1,从1582年10月15日算起(能撑3655年,真是位数多给烧的,1582年起头有意思么)

顺序号: 这16bit则仅用于避免前面的节点标示改变(如网卡改了),时钟系统出问题(如重启后时钟快了慢了),让它随机一下避免重复。

节点标识:48bit,一般用MAC地址表达,如果有多块网卡就随便用一块。如果没网卡,就用随机数凑数,或者拿一堆尽量多的其他的信息,比如主机名什么的,拼在一起再hash一把。

但这里的顺序号并不是我们想象的自增顺序号,而是一个一次性的随机数,所以如果两个请求在同100个纳秒发生时。。。。。

还有这里的节点标识只在机器级别,没考虑过一台机器上起了两个进程。

所以严格的Version1没人实现,接着往下看各个变种吧。

 

2.4 version 1 vs version4

version4随机数的做法简单直接,但以唯一性,时间相关,粗略有序,可反解,可制造做要求的话,则不如version4,比如唯一性,因为只是两个随机long,并没有从理论上保证不会重复。
 

3. Version1变种 - Hibernate

Hibernate的CustomVersionOneStrategy.java,解决了之前version 1的两个问题

  • 时间戳(6bytes, 48bit):毫秒级别的,从1970年算起,能撑8925年....
  • 顺序号(2bytes, 16bit, 最大值65535): 没有时间戳过了一毫秒要归零的事,各搞各的,short溢出到了负数就归0。
  • 机器标识(4bytes 32bit): 拿localHost的IP地址,IPV4呢正好4个byte,但如果是IPV6要16个bytes,就只拿前4个byte。
  • 进程标识(4bytes 32bit): 用当前时间戳右移8位再取整数应付,不信两条线程会同时启动。

顺序号也不再是一次性的随机数而是自增序列了。

节点标识拆成机器和进程两个字段,增加了从时间戳那边改变精度节约下来的16bit。另外节点标识这64bit(1个Long)总是不变,节约了生成UID的时间。

 

4. Version1变种 - MongoDB

MongoDB的ObjectId.java

  • 时间戳(4 bytes 32bit):是秒级别的,从1970年算起,能撑136年。
  • 自增序列(3bytes 24bit, 最大值一千六百万): 是一个从随机数开始(机智)的Int不断加一,也没有时间戳过了一秒要归零的事,各搞各的。因为只有3bytes,所以一个4bytes的Int还要截一下后3bytes。
  •  机器标识(3bytes 24bit): 将所有网卡的Mac地址拼在一起做个HashCode,同样一个int还要截一下后3bytes。搞不到网卡就用随机数混过去。
  • 进程标识(2bytes 16bits):从JMX里搞回来到进程号,搞不到就用进程名的hash或者随机数混过去。

可见,MongoDB的每一个字段设计都比Hibernate的更合理一点,时间戳是秒级别的,自增序列变长了,进程标识变短了。总长度也降到了12 bytes 96bit。

但如果果用64bit长的Long来保存有点不上不下的,只能表达成byte数组或16进制字符串。

 

5. Twitter的snowflake派号器

snowflake也是一个派号器,基于Thrift的服务,不过不是用redis简单自增,而是类似UUID version1,

只有一个Long 64bit的长度,所以IdWorker紧巴巴的分配成:

  • 时间戳(42bit) :自从2012年以来(比那些从1970年算起的会过日子)的毫秒数,能撑139年。
  • 自增序列(12bit,最大值4096):毫秒之内的自增,过了一毫秒会重新置0。
  • DataCenter ID (5 bit, 最大值32):配置值,支持多机房。
  • Worker ID ( 5 bit, 最大值32),配置值,因为是派号器的id,一个机房里最多32个派号器就够了,还会在ZK里做下莫道不消魂注册。

可见,因为是中央派号器,把至少40bit的节点标识都省出来了,换成10bit的派号器标识。所以整个UID能够只用一个Long表达。

另外,这种派号器,client每次只能一个ID,不能批量取,所以额外增加的延时是问题。

 

6. 扩展阅读

《细聊分布式ID生成方法》

《生成全局唯一ID的3个思路,来自一个资深架构师的总结》

 

7. 扩展问题,能不能不用派号器,又一个Long搞定UUID??

这是我们服务化框架一开始的选择,TraceID设为了一个Long,而不是String,又不想用派号器的话,怎么办?

从UUID的128位压缩到Long的64位,又不用中央派号器而是本地生成,最难还是怎么来区分本地的机器+进程号。

 

思路一,压缩其他字段,留足够多的长度来做机器+进程号标识

时间戳是秒级别,1年要24位,两年要25位.....
自增序列,6万QPS要16位,10万要17位...
剩下20-24位,百万分之一到一千六百万分之一的重复率,然后把网卡Mac+进程号拼在一起再hash,取结果32个bit的后面20或24个bit。但假如这个标识字段重复了,后面时间戳和自增序列也很容易重复,不停的重复。

 

思路二,使用ZK 或 mysql 或 redis来自增管理标识号

如果workder字段只留了12位(4096),就要用ZK或etcd,当进程关闭了要回收这个号。
如果workder字段的位数留得够多,比如有20位(一百万),那用redis或mysql来自增最简单,每个进程启动时拿一个worker id。

 

思路三,继续Random

继续拼了,因为traceId的唯一性要求不高,偶然重复也没大问题,所以直接拿JDK UUID.randomUUID()的低位long(按UUID规范,高位的long被置了4个默认值的bit,低位只被设置3个bit),或者不用UUID.randomUUID()了,直接SecureRandom.nextLong(),不浪费了那3个bit。

你猜我们最后选了哪种?

我的公众号: jnby1978,谢谢。

 

by calvin | tags : | 15

一份平民化的应用性能优化检查列表(完整篇)

| Filed under 技术

版权声明:本文版权归 江南白衣@唯品会 所有。任何形式的转载,必须获得作者授权。

作为一份比较初级的检查表,高大上的话不多说了,直接开始。

嗯,还是先关注下我再开始。微信公众号jnby1978。

1.总原则

一些正确但稍显废话的原则,但能指导后面每个章节的优化,所以还是要啰嗦一次。

 

  1. 可扩展性架构,堆机器能不能解决问题是最最优先考虑的问题
  2. 去中心化的点对点通信,优于通过中心代理的通信
  3. 池化的长连接,优于短连接
  4. 二进制数据,优于文本数据
  5. 尽量减少交互,一次调用的粗粒度聚合接口 优于 多次调用的细粒度接口
  6. 尽量减少交互,批量接口优于循环调用
  7. 尽量只交互必要的数据
  8. 尽量就近访问
  9. 尽量使用缓存
  10. 总是设定超时
  11. 在合适的场景,并行化执行
  12. 在合适的场景,异步化执行

2.环境准备

保证符合自家各种规范(没有的话赶紧回家写一个),尤其线下压测服务器的配置要与生产环境一致。

2.1 操作系统

  • 自家规范调优应包含TCP内核参数,网卡参数及多队列绑定,IO&Swap内核参数,ulimit资源限制等。

2.2 JVM与应用服务器

  • 使用JDK7.0 u80 或 JDK8 最新版。
  • 检查JVM启动参数已按自家规范调优,见《关键业务系统的JVM参数推荐》
  • 检查应用服务器(Tomcat或微服务容器) 已按自家指南调优,如线程数等。

2.3 周边依赖系统

  • 检查数据库,缓存,消息系统,已按自家指南调优。

2.4 后台辅助程序

  • 检查日志收集,系统监控等,已使用最新版本,最优配置。
  • 最好其最大消耗已被控制(通过cgroup,taskset等方式)。

2.5 测试程序

  • 压测工具如JMeter,启动参数要参考真实应用客户端的参数优化(如JVM参数,Netty参数等)。
  • 测试脚本和客户端程序经过review,不存在影响性能的步骤,不存在System.out.println()等明显的瓶颈。

2.5 流量模型

  • 扇入模型:平时与高峰期的流量估算,各接口的流量比例,响应时间要求
  • 扇出模型:各接口对远程服务、数据库、缓存、消息系统的调用比例,响应时间估算。

 

大家在心里都有这么一个大概的模型,但很少认真写出来。

行文到此,大家大概可以感受到这份checklist的风格,都是大家明白的道理,但可能一时也会忘掉的,这里啰啰嗦嗦的給写下来。


3.数据库

特别鸣谢,我司DBA。

3.1 拓扑

根据扩展性原则考虑:

  • 垂直拆分:按业务将不同的表拆分到不同的库。
  • 水平拆分:水平分库分表。
  • 读写分离:在业务允许的情况下,在从库读取非实时数据。

3.2 Schema

自家规范应包含:

  • 统一的存储引擎,主键策略。
  • 禁用存储过程,函数,触发器,外键约束。
  • 列类型永远越短越好,建议:布尔/枚举:tinyint,日期与时间戳:timestamp或int,char/text/blob: 尽量用符合实际长度的varchar(n),小数及货币:移位转为int 或 decimal,IP地址:int。
  • 索引策略:索引字段的顺序需要考虑字段值去重之后的个数,较多的放前面,合理创建联合索引,避免冗余索引,合理利用覆盖索引等。

 

3.3 SQL

1. 自家规范应包含:

  • 如禁止多于3表join,禁用子查询
  • 禁止where子句中对字段施加函数,如to_date(add_time)>xxxxx
  • 避免MySQL进行隐式类型转化,如ISENDED&eq;1 与 ISENDED&eq;`1`
  • 不建议使用%前缀模糊查询,模糊查询较多时建议使用ElasticSearch

 

根据尽量少数据原则与尽量少交互的原则来设计SQL:

  • 禁止select
  • 合理的SQL语句,减少交互次数

根据扩展性原则,将负载放在更容易伸缩的应用服务实例上:

  • 尽量不要做数学薄雾浓云愁永昼运算,函数运算, 或者输出格式转换等非必要操作
  • 避免count(*),计数统计实时要求较强使用memcache或者redis,非实时统计使用单独统计表,定时更新。
  • 甚至排序都是不鼓励的,尽量在应用侧进行。另外避免多余的排序,使用GROUP BY 时,默认会进行排序,当你不需要排序时,可以使用order by null。

2. 联系DBA进行MySQL统计的慢查询的Review,解析SQL查询计划时尽量避免extra列出现:Using File Sort,Using Temporary

3.4 DAO框架

  • 根据尽量少交互与尽量少数据的原则,需使用对SQL完全可控的DAO框架,建议为MyBatis 或 Spring JDBC Template。
  • 必须使用prepareStatement,提升性能与防注入。
  • 根据一切皆有超时的原则,配置SQL执行的超时。可在连接池里设置default值,可在MyBatis的Mapper定义里可设置每个请求的超时,可惜规范是秒级的。
  • JDBC driver 规范本身不支持异步模式,如果一定要异步,可以像Quasar那样把请求封装成Callable交给另外的线程池执行,但要注意其额外开销。

 

3.5 事务

  • 不使用事务,连接池设置autocommit,使用其他方式来保持数据一致性。
  • 通过Transaction Annotation控制事务,事务跨度尽量短,把非事务范围内的业务逻辑剔除到被标注的函数之外。
  • 只读事务可以不加事务标注。

 

连接池

选型:

  • 在分库分表时,根据点对点通信优先的原则,尽量使用客户端分片的实现。功能不满足时才用MyCat中央代理。
  • 推荐使用性能最高HikariCP,或者Druid,不推荐c3p0与DBCP。

 

连接池的配置:

  • 配置初始值,再联系DBA获得线上数据库支持的连接数,计算最大连接数。
  • 连接有效性检查,只在连接空闲检测时执行,不在拿出和归还连接时执行,最好是直接使用数据的Ping方案,不要配置检查SQL
  • 根据总是设置超时的原则,配置获取连接超时的时间。
  • 配置合理的空闲连接回收间隔和空闲时间。

番外篇:在分库分表时,可考虑基于HikariCP二次开发,减少总的空闲连接检查线程数(比如128个分区,可能有256条线程),重用同一个实例上的库的连接等。


4.缓存

 

4.1 多级缓存

 

  • 根据缓存原则, 缓存 > 数据库/远程调用
  • 根据就近原则, 堆内缓存 > 堆外缓存 > 集中式缓存
  • 堆内缓存受大小限制,并影响GC
  • 堆内缓存与堆外缓存,分布在每一台应用服务器上,刷新方式比集中式缓存复杂
  • 堆外缓存与集中式缓存,需要序列化/反序列化对象
  • 集中式缓存,有网络传输的成本,特别是数据超过一个网络包的大小。
  • 集中式缓存,一次获取多个键时,在有分区的情况下,需要收发多个网络包。

使用上述条件选择合适的缓存方案,或同时使用多级缓存,逐层回源。

 

4.2 综述

  • 需要对回源进行并发控制,当key失效时,只有单一线程对该key回源。
  • 基于二进制优于文本数据的原则,JSON的序列化方案较通用与更高的可读性。而对于较大,结构较复杂的对象,基于Kyro,PB,Thrift的二进制序列化方案的性能更高,见后面的序列化方案部分。

 

4.3 堆内缓存

选型:

  • 推荐Guava Cache。
  • Ehcache较重,性能也较差。更不要使用存在严重bug的Jodd Cache。

GuavaCache:

  • 正确设置并行度等参数。
  • 重载load()参数,实现单一线程回源。
  • Guava Cache能后台定时刷新,在刷新的过程中,依然使用旧数据响应请求,不会造成卡顿,但需要重载实现reload()函数。
  • Guava Cache同时还支持并发安全版的WeakHashMap。

 

4.4 堆外缓存

选型:

  • 推荐Cassandra的OHC 或者 OpenHFT的Chronical map2。
  • OHC够简单,其实R大不喜欢Chronical,玩的太深,换个JDK都可能跑不起来。
  • Chronical map3的license则较不友好,复杂度高且要求JDK8。
  • 其他的Ehcache的Terracota Offheap 一向不喜欢。

4.5 Memcached

​客户端:

  • 基于点对点通信优于网关的原则,使用客户端一致性哈希分区。
  • 推荐Spymemcached。 XMemcached 太久没更新,Folsom知名度不高。
  • 注意Spymemcached为单线程单连接架构(一个MemcachedClient只有一条IO线程,与每台Memcached只有一条连接),必要时可多建几个MemcachedClient随机选择,但不要用Commons Pool去封装它,把Spy原本的设计一笔抹杀。
  • 根据在合适场景使用并发的原则,Spymemcached支持异步API。
  • 根据一切皆设超时的原则,可在连接工厂中设置最大超时数,默认值两秒半太长。

 

数据结构:

  • Key必须设置失效时间。
  • Key必须有长度限制。
  • Value长度需要控制,以不超过1个网络包(MTU,千五字节)为佳。
  • Value大小差别较大的缓存类型,建议拆分到不同MC集群,否则会造成低使用率并且产生踢出。

 

4.6 Redis as Cache

 

Redis拓扑:

基于点对点通信优于网关的原则,使用如下两种拓扑

  • 无HA的普通分片:由Jedis客户端完成分片路由。
  • Redis Cluster:同样由Jedis客户端封装分区,跳转,重试等逻辑,需要使用最新版的Jedis版本。

 

服务端:

  • Cache节点与持久化数据节点不要混用。
  • Cache节点是否需要持久化要仔细衡量。
  • 由于Redis是单线程,使用taskset进行cpu绑定后可以有效地利用cpu,并在单机上运行多个redis实例。
  • 对热键进行监控,发现不合理的热健要进行分拆等处理。

客户端:

  • Jedis基于Apache Commons Pool进行了多连接的封装,正确配置总连接数不超过Redis Server的允许连接数。
  • 性能考虑,空闲连接检查不要过于频繁(建议30秒以上),另不要打开testOnBorrow等测试参数。
  • 根据一切皆有超时的原则,设定统一的调用超时,获取连接的最长等待时间参数,重试次数
  • 根据在合适的地方异步的原则,Jedis本身没有异步API,只在PipleLine模式下支持。

数据结构:

  • 必须对Key设置失效时间。
  • Key必须有长度限制。
  • Value长度需要控制,不要超过一个网络包。另外集合的元素不要超过五千个。
  • 除了使用序列化的String,同样可以考虑用Hash来存储对象,注意内部结构为ZipList与HashTable时,hmget 与hgetall的不同复杂度。

命令:

  • 慎用的命令:LANGE(0, -1), HGETALL, SMEMBER
  • 高复杂度的命令: ZINTERSTORE, SINTERSTORE, ZUNIONSTORE, ZREM
  • 尽量使用多参数的命令:MGET/MSET,HMGET/HMSET, LPUSH/RPUSH, LRANGE
  • 尽量使用pipeline
  • 根据减少交互的原则,必要时可使用Redis的Lua脚本

 


5.服务调用

5.1 接口设计

1. 尽量少交互的原则:

支持批量接口,最大的批量,综合考虑调用者的需求与 后端存储的能力。

支持粗粒度接口,在支持原子细粒度接口的同时,支持粗粒度接口/聚合层接口,将多个数据源的获取,多个动作,合并成一个粗粒度接口。

 

2. 尽量少数据的原则:

在提供返回所有数据的大接口的同时,提供只提供满足部分调用者需要的轻量接口。

最好再提供能定制返回字段的接口。

 

3. 二进制数据优于文本数据

同样是一个简单通用性,与性能的选择,特别是大数据量时。

 

5.2 RESTful

仅以Apache HttpClient为例,大部分Restful框架都是对Apache HttpClient的封装。

另外OkHttp也值得看看。

 

  • 不要重复创建ApacheClient实例,使用连接池,正确配置连接池的连接数。
  • 连接池总是有锁,针对不同的服务,使用不同的Apache HttpClient实例,将锁分散开来。在高并发时比使用全局单例的ApacheClient,有很大的性能提升。
  • 根据一切调用皆有超时的原则,每次调用均设置超时时间。RequestConfig里共有Connect Timeout, Socket Timout 和 从Pool中获取连接的Timeout三种超时。
  • 需要异步或并行的场景,使用Apache AsyncHttpClient项目。但要注意AsyncHttpClient项目,检查调用超时的周期默认为1秒。

 

5.3 自家RPC框架

 

每家的RPC框架特性不同,但考虑点都类似。

 

 


6.消息异步

 

6.1 选型

  • 根据就近原则,可以先尝试用JVM内的队列来解决,然后再考虑中央消息系统。
  • 可靠性要求极高的选择RabbitMQ,可支持单条消息确认。
  • 海量消息场景,允许极端情况下少量丢失则使用Kafka。

 

6.2 Kafka

  • 在同步和异步之间做好权衡,异步批量发送可以极大的提高发送的速度。
  • 关注消费者如下参数:commitInterval(自动提交offset间隔),prefetchSize(指单次从服务器批量拉取消息的大小),过大和过小都会影响性能,建议保持默认。

 

6.3 RabbitMQ

  • 根据扩展性原则,RabbitMQ本身没有分片功能,但可以在客户端自行分片。
  • 如非必要情况,应该保持默认的同步发送模式。
  • 关注消费者如下参数:autocommit(自动提交确认,默认false) ,在消息拉取到本地即认为消费成功,而不是真正消费成功后提交。prefetchCount(预取消息条数,默认64条)
  • 生产者在必要时也可以临时降级不进行confirm。

 


7. 日志

 

7.1 综述

  • Log4j2或logback,不要再使用Log4j。
  • 除了应用启停日志,不允许使用超慢的System.out.println() 或 e.printStack();
  • 严格控制日志量避免过高IO,对海量日志,应该有开关可以动态关停。
  • 如果可能出现海量异常信息,可仿效JDK的优化,用RateLimiter进行限流,丢弃过多的异常日志。

7.2 内容

  • 严格控制日志格式,避免出现消耗较大的输出如类名,方法名,行号等。
  • 业务日志不要滥用toJSONString()来打印对象,尽量使用对象自身的toString()函数,因为JSON转换的消耗并不低。
  • 在生产环境必定输出的日志,不要使用logger.info("hello {}", name)的模式,而是使用正确估算大小的StringBuilder直接拼装输出信息。

 

7.3 异步日志

  • 同步日志的堵塞非常严重,特别是发生IO的时候,因此尽量使用异步日志。
  • Logback的异步方案存在一定问题,需要正确配置Queue长度,阀值达到多少时丢弃Warn以下的日志,最新版还可以设置如果队列已满,是等待还是直接丢弃日志。
  • 如果觉得Logback的异步日志每次插入都要询问队列容量太过消耗,可重写一个直接入列,不成功则直接丢弃的版本。

 


8. 工具类

 

8.1 JSON

  • 使用Jackson 或 FastJSON。GSON的性能较前两者为差,尤其是大对象时。
  • 超大对象可以使用Jackson或FastJSON的流式 API进行处理。
  • 将不需要序列化的属性,通过Annotation排除掉。

FastJson:

  • 尽量使用最新的版本。
  • SerializerFeature.DisableCircularReferenceDetect 关闭循环引用检查。

Jackson:

  • 设置参数,不序列化为空的属性,等于默认值的属性。
  • 除了jackson-databinding,可试用简化版没那么多花样的jackon-jr。

 

8.2 二进制序列化

 

需要定义IDL的PB与Thrift,不需要定义的Storm等用的Kyro 都可选择,其他一些比较旧就算了。

8.3 Bean复制

在VO,BO之间复制时,使用Orika(生成代码) 或 Dozer(缓存反射),不要使用需要每次进行反射的Apache BeanUitls,Spring BeanUtils。

 

8.4 日期

JDK的日期类与字符串之间的转换很慢且非线程安全。

继续用Java日期不想大动作的,就用CommonsLang的FastDateFormat。

能大动作就用joda time,或者JDK8的新日期API。

 


9.Java代码优化 与 业务逻辑优化

 

参考《Java调优指南1.8版》,对内存使用,并发与锁等方面进行优化。

 

规则前置,将消耗较大的操作放后面,如果前面的条件不满足时可。

另外前面提到的一堆原则,比如尽量缓存,尽量少交互,尽量少数据,并行,异步等,都可在此使用。

 

之前的打赏已经花完了,被迫继续卖文赚零花,各位看官再赏两块五呗。

 

转载请保留原文链接: http://calvin1978.blogcn.com/articles/checklist.html

by calvin | tags : | 11

Java性能优化指南1.8版,及唯品会的实战

| Filed under 技术

来了唯品会一年多,不少时间花在与服务化框架、业务应用的性能的缠斗上。前几天正好趁着中生代社区的十月十城技术沙龙,把脑海中关于性能优化的记忆全部理了一遍....

各位客官,又又又更新了,因为流播甚广,所以唯恐言之未尽,更怕流毒网络,所以周末又赶紧再修订了一次。1.8版的更新记录见最后一页,超链接也已修好。

2017的最新版,请看公众号“花钱年华”里的文章,不在这边也贴一遍图了,累。

最后,再广告一次,公众号"春天的旁边"新张,微信号jnby1978,敬请查找订阅获得本指南的最新版本通知。或手机扫描下面的二维码:

转载请保留原链接: http://calvin1978.blogcn.com/articles/javatuning.html

Java后端,应该日常翻看的中文技术网站

| Filed under 技术

你还在学习吗?

 

1.内容生产者

  • InfoQ
    中文技术第一站,佩服霍老板,真金白银地为中国程序员们生产内容。
  • ImportNew
    专门面向Java的内容生产者兼聚合者,偶然也有些面向入门的小白文。
  • 并发编程网
    面向高并发,Java,开源的社区。

 

2. 内容聚合者

现在写博客,没人用RSS读了,在微博里的传播也就一两天,好在有它们负责挖掘整理散落在各个角落的技术文章。

 

3. 还坚持在博客为主的同学

 

4. 公众号

大家现在都习惯用手机了,尽管公众号有不方便搜索,文章后续更新,不給放链接等种种不好。

上面各个媒体的公众号,还有像池老师的MacTalk,西乔女神的神秘的程序员,朱女神的嘀嗒嘀嗒,小道消息这些著名公号,就不啰嗦了。

1. 高可用架构,Tim创立的高可用社区,线上线下分享的架构方面文章整理
高可用架构

2. 中生代技术,新晋的中生代技术社区,线上线下分享文章的整理
中生代技术

3. 聊聊架构,InfoQ的架构方面精选文章
聊聊架构

4. 你假笨,寒泉子,阿里JVM团队,JVM的各种细节
你假笨

5. HelloJava by 毕玄,bluedavy,阿里大神,Java问题排查的各种Case
HelloJava

6. 开涛的博客 by 开涛,京东
开涛的博客

7. 美团点评技术团队 by 美团点评
美团点评技术团队

8. 架构师之路 by 沈剑, 58
架构师之路

9. 春天的旁边 by 江南白衣,唯品会
春天的旁边

10. 写程序的康德
写程序的康德

11. 瞬息之间 by mindwind,京东
瞬息之间

12. 开发资讯 by  臧秀涛,InfoQ ,低调无比的公号名
开发资讯
大家可以在评论里留下你的推荐。

by calvin | tags : | 14

我的Java后端书架 (2016年暖冬版)

| Filed under 技术

本书架主要针对Java后端开发与架构。

更新记录:4.0版把第五部份-具体技术的书整块拿掉了。《TCP/IP详解 卷1:协议》出到了第二版,增加《SRE:Google运维解密》,《Java8 实战》。

更偏爱那些能用简短流畅的话,把少壮不努力的程序员所需的基础补回来的薄书,而有些教课书可能很著名,但干涩枯燥,喋喋不休的把你带回到大学课堂上昏昏欲睡,不录。
 

1. 操作系统与网络的书

《Linux内核设计与实现 第3版》
Robert Love用最薄的篇幅,顺畅的文字将Linux内核主要的算法讲清楚了,《深入理解Linux内核》《深入Linux内核架构》之类厚厚的全是代码,不是专门的内核程序员看这本足够了。

《Linux系统编程 第2版》
继续是Robert Love,比起APUE也是以薄见长,专门针对重要的系统调用讲解。

《性能之巅》
操作系统的性能调优、监控、工具和方法莫道不消魂论,看这本就够了,已经足够厚,可能是书单里最厚的一本。

《TCP/IP详解 卷1:协议》
这么多年过去了,TCP的书好像主要还是只有这一本,有点旧了,看了也还是半懂不懂的。后人在2011年写了第二版。

《WireShark网络分析就这么简单》《WireShark网络分析的艺术》
多少人,是看了这两本轻松又实战的书,才真正理解TCP的细节。

PS:《UNIX环境高级编程》《UNIX网络编程》,APUE和UNP更多作为一本超厚工具书存在。《Unix 编程艺术》,扯的都是闲篇,厚厚的一本其实略读一下就行。 《现代操作系统 第3版》如果看LKD未尽兴,可以回头看看这本基础概念,感觉比那本枯燥的《操作系统概念》(恐龙书)读起来舒服。

《TCP/IP指南》 前面wireshark书作者的推荐,网上有英文免费版,然后有中文版的卷1卷2,但可能那么多章节那么厚你只关心TCP和HTTP两部分。《HTTP权威指南》,同样是自己从厚厚的目录里挑选感兴趣的章节来看。

另外,那些日本韩国人写的《图解XXX》感觉都不喜欢。

 

2. 算法的书

《数据结构与算法分析-Java语言描述 第3版》
够薄,数据结构与算法分析的点基本都涵盖了,而且喜欢它的示例代码是Java写的,新出了第3版。

《算法 第4版》
可与上一本对比着读,厚一些,也多些图,但知识点没上面的全,也是Java的。

PS: 《数学之美》《编程珠玑》,都是专栏文章,讲得并不系统,可以当兴趣读物来看。

数学系偏爱无比枯燥很多公式的《算法导论》, 计算机系喜欢这本实用主义的《算法设计与分析基础 第3版》

 

3. 架构设计的书

《软件系统架构:使用视点和视角与利益相关者合作 第2版》
也是教科书,最难得的是,这本老书在十年后的去年升级了第二版,所以感觉鲜活了好多,也许是最鲜活的一本架构书。

《恰如其分的软件架构 - 风险驱动的设计方法》
由于人类与生俱来的惰性,计算机原本科学的、精准的设计方式,有了敏捷的借口之后就很难再维持了。本书就是在这种背景下,提出由风险来决定设计的度。除了开始的风险驱动部分,其余部分就是规规矩矩标标准准的架构师教科书。

《SRE:Google运维解密》
广告词是 “地球上有这么一个团队,将运维推向极限高度”,稍嫌标题党,很多值得翻的东西。

《发布!软件的设计与部署》
关于高可靠性的软件,学校里不会教,出来社会却要面对的那部分,英文的原标题更清晰。

《大型网站技术架构:核心原理与案例分析》
淘宝出品,大型互联网站的科普入门书。

《高扩展性网站的50条原则》
同是入门级读物,如果还有个高可用50条原则,那就齐了。

《微服务设计》
那么多微服务的书,还是这本比较不像赚快钱的。

《大数据日知录》
前几年参加各种技术会议,CAP,最终一致性,RWN,向量时钟,Paxos,一致性哈希,Gossip什么的能灌你一耳朵。而现在,你只要在家安安静静的看书就够了。不过这个领域发展太快,又一年过去了,期望它可以持续出新版。

PS: 关于设计模式,我以前曾经有过很多很多本,GOF23啦,企业应用架构模式啦,EIP啦, POSA 5卷本啦,反模式啦,JavaEE/SOA/Restful的模式啦。但现在觉得对新人来说,一本Java写的《Head First 设计模式》,知道什么叫设计模式就够了。

《程序员必读之软件架构》作者维护着codingthearchitecture.com 。不过中文书名叫“必读”有点过。

 

4. Java语言的书

《Java并发编程实战》
Java并发经典,人手一本不用多说了。

《实战Java高并发程序设计》
国人新作,流畅易读,内容也比上面一本来得新。

《Java8 实战》
Java8的新特性讲得最全最仔细的。

《深入理解 Java 虚拟机 第2版》
理解虚拟机并不是那么难,Java程序员来说,很多知识其实是必须的。另外还有几本类似主题的书,忽然一下子都出来了。

《Java性能权威指南》
比起多年前那部调优圣经,讲得更加深入,也更加贴近现在的JDK。可以从里面挑些知识点来,做Java调优的面试题。

《有效的单元测试》
不同于那些动辄BDD的高深书籍,专注于如何写“好”的,可维护的单元测试,拿来给团队看,能省很多口水。

PS:《Effective Java》外界一致推崇,但有点太过誉了。另外《Thinking in Java》有点旧了,而且作者思路随意,译者语言晦涩,新程序员还是建议同时再看两卷《Java核心技术 - Core Java》

 

6. 程序员的自我修养

PS. 最近没买什么新书,随便说点旧书:

《程序员修佳节又重阳炼之道-从小工到专家》,Pragmatic Programmer-注重实效的程序员开山之作,翻译的马达维文笔也和熊节一样好。

《代码整洁之道》和 《程序员的职业素养》,英文名是很相近的《Clean Code》和 《Clean Coder》,应该接替《代码大全2》成为必看的系列,因为后者太厚了,而且也有不少过时的东西,要自己去过滤。

《重构》很厚,但最有价值就是前面几章的洗东篱把酒黄昏后脑篇,具体实作不如薄薄的《重构手册》

关于敏捷的书,最开始的那本《解析极限编程--拥抱变化》就很好,再随便找本Scrum的流程看看就够了,《敏捷开发的艺术》也不错。

《布道之道》,经常在组织里推行新技术的同学可以看下,七种怀疑论者模式,脑海中一幅幅熟悉的面孔。

PS. 温伯格的书网上很推崇,《成为技术领佳节又重阳导者》之类的,但我觉得年代太远,读起来其实没多大意思,一两个鸡汤观点还要自己从书里慢慢淘,有那功夫不如看点别的。

 

新公众号"春天的旁边"开张,欢迎查找订阅。或手机扫描下面的二维码:

文章持续修订,转载请保留原链接: http://calvin1978.blogcn.com/articles/bookshelf16.html

 

by calvin | tags : | 23

SecureRandom的江湖偏方与真实效果

| Filed under 技术

SecureRandom,我们一般都知道江湖偏方 -Djava.security=file:/dev/./urandom,但往往不求甚解。一年前,在那个有点暗的办公室里,我就是这么做的。

一年后,又有同学说JDK8下Thread Dump出很多SecureRandom的BLOCKING。這回怒翻JDK代码,并配合JMH写的测试,总结出这么一篇。

1. /dev/random 与 /dev/urandom

Linux的两个随机数源, 从IO中断,网卡传输包这些外部入侵者不可预测的随机源中获取熵,混合后使用CSPRNG生成熵池。

当熵池估值为0时,/dev/random 会block住请求,而/dev/urandom 则会继续输出随机数。

 

2. JDK7的SecureRandom

2.1 generateSeed()与next*()

generateSeed()可以为其他安全算法生成种子,随机度需求更高些。
nextInt(), nextLong()则是我们更关心的生成随机数,最后都是调用nextBytes()。

 

2.2 seedSource

seedSource由两者决定,首先看 -Djava.security.egd ,
没设置则看jre/lib/security/java.security,JDK7中securerandom.source=file:/dev/urandom

 

2.3 SHA1PRNG 与 NativePRNG

从名字就知道,两者都是伪随机算法。另外,两种算法都会有synchronized关键字,都会有阻塞,只有时间长短的不同。

 

2.3.1 SHA1PRNG

generateSeed()的实现:只要配置的seedSource是/dev/random 或 /dev/urandom,就会使用NativeSeedGenerator,而此君在JDK7里有bug,只从/dev/random 取值,有可能被阻塞。

nextBytes()的实现: 纯Java实现,通过不断的对当前Hash值进行再一次SHA1哈希而成。

那Hash的初始值怎么来?如果没有被外部显式设置,则用下面比较复杂的算法生成(可跳过不看)。

  • 先有个SecureRandom seeder,并且用java从系统收集到一些噪音作为这个SR的初始seed。
  • 调用generateSeed() , 获得一个seed,可能被阻塞(见上)。
  • 调用seeder.setSeed(seed) ,合并1和2的seed。
  • 最后调用seeder.nextBytes(),生成最后的seed。

 

2.3.2 NativePRNG

generateSeed()的实现:从/dev/random中取值,可能阻塞。

nextBytes()的实现:从/dev/urandom 中取值,再异或 SHA1PRNG生成的随机值而成。

可见NativePRGN的性能一定会比单纯SHA1PRNG差,synchronized的代码也多一倍。

那为什么要异或 SHA1PRNG呢?为了支持setSeed(),/dev/[u]random都是不可写的,只好再引入一个可设置seed的SHA1PRNG。

/dev/[u]random不需要Java应用来給种子,而SHA1PRNG则从/dev/urandom中获得种子并显式设置,也就不需要2.3.1中所述的种子四步曲,所以不会阻塞。

 

2.3.3 算法的选择

如果用getInstance()获取,则返回的是特定算法的实现。

如果用new SecureRandom(), 则看seedSource的设置,如果是/dev/[u]random 之一则是NativePRNG,否则是SHA1PRNG,比如-Djava.security=file:/dev/./urandom,多了个./在中间, 就成了SHA1PRNG了。

 

3. 江湖偏方的诞生

在JDK7,默认算法是NativePRNG,里面/dev/urandom本身不用seed,而用到的SHA1PRNG的初始seed也从/dev/urandom 读取,不存在启动慢的问题。就是消耗和延时比纯SHA1PRNG大。

然后Tomcat 生成sessionId时显式使用了SHA1PRNG,因为NativeSeedGenerator的bug,此时初始seed要从/dev/random读取,就存在启动慢的可能(见2.3.1)。所以要设置seedSoure而且要加个./在中间,绕过NativeSeedGenerator改为用URLSeedGenderator。

如果一个不明真莫道不消魂相的群众,也跟着设置-Djava.security=file:/dev/./urandom, 一个意外的效果就是,默认的算法也变成SHA1PRNG了。

 

4. JDK8的SecureRandom

首先,Native算法多了两种子类型。NativeBlocking的generateSeed与nextBytes都从/dev/random中读,NativeNonBlocking则两者都从/dev/urandom中读。

不过Native里nextBytes并不需要调用generateSeed,所以对于主要用SecureRandom来生成随机数的应用来说,这个区别不大。

其次,SHA1PRNG用到的SeedGenerator,终于改好了,原来NativeSeedGenerator无论设什么都是读/dev/random,现在改为设什么就读什么,所以jre/lib/security/java.security 里也改了 securerandom.source=file:/dev/random

所以,JDK8里,如果你显式获得的SHA1PRNG以后启动不想有阻塞的可能性,还是要设成-Djava.security=file:/dev/urandom,只是不用猥琐的加个. 在中间了。不过依然加上也无不可。

如果你想把默认算法搞成SHA1PRNG,那还是要继续江湖偏方。

 

5. 结论

  • SHA1PRNG 比 NativePRNG消耗小一半,synchronized的代码少一半,不与系统/dev/urandom交互所以偶发高延时也更少一些,所以没特殊安全要求的话建议用SHA1,比如生成sessionId,traceId的场景
  • 如果想用SHA1, 设成-Djava.security=file:/dev/./urandom总是对的
  • 如果想用Native,什么都不设置就好了。
  • 如果自己能控制,在应用或框架启动时,先调用一下相应SecureRandom算法实例的nextInt()函数,总能减少一点首次服务调用所花的时间。

 
附录:基于JMH的测试结果, 24核机器上,48线程并发获取secureRandom.nextLong()的测试。

App.randomWithNative 379862 QPS
App.randomWithNative:randomWithNative·p0.00 ≈ 10⁻⁴ ms/op
App.randomWithNative:randomWithNative·p0.50 0.001 ms/op
App.randomWithNative:randomWithNative·p0.90 0.007 ms/op
App.randomWithNative:randomWithNative·p0.95 0.007 ms/op
App.randomWithNative:randomWithNative·p0.99 0.008 ms/op
App.randomWithNative:randomWithNative·p0.999 sample 56.254 ms/op
App.randomWithNative:randomWithNative·p0.9999 sample 186.945 ms/op
App.randomWithNative:randomWithNative·p1.00 sample 1235.223 ms/op

App.randomWithSHA1 668574 QPS
App.randomWithSHA1:randomWithSHA1·p0.00 ≈ 10⁻⁴ ms/op
App.randomWithSHA1:randomWithSHA1·p0.50 0.002 ms/op
App.randomWithSHA1:randomWithSHA1·p0.90 0.006 ms/op
App.randomWithSHA1:randomWithSHA1·p0.95 0.321 ms/op
App.randomWithSHA1:randomWithSHA1·p0.99 1.701 ms/op
App.randomWithSHA1:randomWithSHA1·p0.999 2.245 ms/op
App.randomWithSHA1:randomWithSHA1·p0.9999 12.325 ms/op
App.randomWithSHA1:randomWithSHA1·p1.00 138.936 ms/op

 
中秋寫的文章,修啊改啊才發出來,但這麼偏的題材,估計也就只有Google的爬蟲會看了。

by calvin | tags : | 8

Btrace入门到熟练小工完全指南

| Filed under 技术

BTrace是神器,每一个需要每天解决线上问题,但完全不用BTrace的Java工程师,都是可疑的。

BTrace的最大好处,是可以通过自己编写的脚本,获取应用的一切调用信息。而不需要不断地修改代码,加入System.out.println(), 然后重启,然后重启,然后重启应用!!!

同时,特别严格的约束,保证自己的消耗特别小,只要定义脚本时不作大死,直接在生产环境打开也没影响。

在网上搜索BTrace出来的文章都有点旧了,而且不够详细,于是决定,重新写一份。

码这么多的字好辛苦,请保留原文链接:http://calvin1978.blogcn.com/articles/btrace1.html

 

1. 概述

1.1 快速开始

BTrace搬家了!! 已经搬离了Sun,搬到了http://github.com/btraceio/btrace,目前的版本已经是1.38。

在Release页面里下载最新Zip版,解压就能用,UserGuide和Samples也在里面。

先抄一个UserGuide里的例子:

import com.sun.btrace.annotations.*;

import static com.sun.btrace.BTraceUtils.*;


@BTrace

public class HelloWorld {

    @OnMethod(clazz="java.lang.Thread", method="start")

    public static void onThreadStart() {

        println("thread start!");

    }

}

然后ps找出要监控的java应用的pid, ./btrace $pid HelloWorld.java 就跑起来了。

是不是很简单??基本上不用任何BTrace的知识,都能猜出HelloWorld会干啥。通过JVM Attach API,btrace把自己绑进了被监控的进程,按HelloWorld.java里的定义,进行AOP式的代码植入。

最开心就是这里,如果还想监控其他内容,直接修改HelloWorld.java,再执行一次btrace就可以了,不需要重启应用!! 重启应用!!

 

1.2 典型的场景

1. 服务慢,能找出慢在哪一步,哪个函数里么?

2. 谁调用了System.gc(),调用栈如何?

3. 谁构造了一个超大的ArrayList?

4. 什么样的入参或对象属性,导致抛出了这个异常?或进入了这个处理分支?

 

1.3 一些重要的事

为了避免Btrace脚本的消耗过大影响真正业务,所以定义了一系列不允许的事情:比如不允许调用任何类的任何方法,只能调用BTraceUtils 里的一系列方法和脚本里定义的static方法。 比如不允许创建对象,比如不允许For 循环等等,更多规定看User Guide。

当然,可以用-u 运行在unsafe mode来规避限制,但不推荐。

在以前的例子里,甚至还不能字符串相加,必须用strcat:

println(strcat(strcat(probeClass, "."), probeMethod));

好在新版里已经可以写回:

println(probeClass + '.' + probeMethod);

另外,BTrace植入过的代码,会一直在,直到应用重启为止。所以即使Btrace推出了,业务函数每次执行时都会多出一次Btrace是否Attach状态的判断。

最后,记得用Eclipse,而不是写字板来写脚本。

 

1.4 其他命令行选项

1.4.1 定义classpath

如果在HelloWorld.java里使用了JDK外的其他类,比如Netty的:

./btrace -cp .:netty-all-4.0.41.Final.jar $pid HelloWorld.java

但上面定义的classpath只在编译脚本时使用,而脚本里需要显式使用非JDK类的机会其实很少(后面真正用到的时候会提起)。
而在运行时,因为已经绑到目标应用的JVM里,用的是目标JVM的classpath。

1.4.2 结果输出到文件

./btrace -o mylog $pid HelloWorld.java

很坑新人的参数,首先,这个mylog会生成在应用的启动目录,而不是btrace的启动目录。其次,执行过一次-o之后,再执行btrace不加-o 也不会再输出回console,直到应用重启为止。

所以有时也直接用转向了事:
./btrace $pid HelloWorld.java > mylog

 

1.4.3.预编译脚本

虽然btrace可以实时编译Java源文件,但如果你的脚本是要给运维同学执行的,线上运行时才发现写错了就尴尬了。此时可以用btracec命令预编译一下:

./btracec HelloWorld.java

 

2. 拦截方法定义

2.1 精准定位

就是HelloWorld的例子,精确定义要监控的类与方法。

 

2.2 正则表达式定位

可以用表达式,批量定义需要监控的类与方法。正则表达式需要写在两个 "/" 中间。

下例监控javax.swing下的所有类的所有方法....可能会非常慢,建议范围还是窄些。

@OnMethod(clazz="/javax\\.swing\\..*/", method="/.*/")

public static void swingMethods( @ProbeClassName String probeClass@ProbeMethodName String probeMethod) {

   print("entered " + probeClass + "."  + probeMethod);

}

通过在拦截函数的定义里注入@ProbeClassName String probeClass, @ProbeMethodName String probeMethod 参数,告诉脚本实际匹配到的类和方法名。

另一个例子,监控Statement的executeUpdate(), executeQuery() 和 executeBatch() 三个方法,见JdbcQueries.java

 

2.3 按接口,父类,Annotation定位

比如我想匹配所有的Filter类,在接口或基类的名称前面,加个+ 就行
@OnMethod(clazz="+com.vip.demo.Filter", method="doFilter")

也可以按类或方法上的annotaiton匹配,前面加上@就行
@OnMethod(clazz="@javax.jws.WebService", method="@javax.jws.WebMethod")

 

2.4 其他

1. 构造函数的名字是 <init>
@OnMethod(clazz="java.net.ServerSocket", method="<init>")

2. 静态内部类的写法,是在类与内部类之间加上"$"

@OnMethod(clazz="com.vip.MyServer$MyInnerClass", method="hello")

3. 如果有多个同名的函数,想区分开来,可以在拦截函数上定义不同的参数列表(见4.1)。

 

3. 拦截时机

可以为同一个函数的不同的Location,分别定义多个拦截函数。

3.1 Kind.Entry与Kind.Return

@OnMethod( clazz="java.net.ServerSocket", method="bind" )
不写Location,默认就是刚进入函数的时候(Kind.ENTRY)。

但如果你想获得函数的返回结果或执行时间,则必须把切入点定在返回(Kind.RETURN)时。

OnMethod(clazz = "java.net.ServerSocket", method = "getLocalPort", location = @Location(Kind.RETURN))

public static void onGetPort(@Return int port, @Duration long duration)

duration的单位是纳秒,要除以 1,000,000 才是毫秒。
 

3.2 Kind.Error, Kind.Throw和 Kind.Catch

异常抛出(Throw),异常被捕获(Catch),异常没被捕获被抛出函数之外(Error),主要用于对某些异常情况的跟踪。

在拦截函数的参数定义里注入一个Throwable的参数,代表异常。

@OnMethod(clazz = "java.net.ServerSocket", method = "bind", location = @Location(Kind.ERROR))

public static void onBind(Throwable exception, @Duration long duration)



3.3 Kind.Call与Kind.Line

下例定义监控bind()函数里调用的所有其他函数:

@OnMethod(clazz = "java.net.ServerSocket", method = "bind", location = @Location(value = Kind.CALL, clazz = "/.*/", method = "/.*/", where = Where.AFTER))

public static void onBind(@Self Object self, @TargetInstance Object instance, @TargetMethodOrField String method, @Duration long duration)

所调用的类及方法名所注入到@TargetInstance与 @TargetMethodOrField中。

​静态函数中,instance的值为空。如果想获得执行时间,必须把Where定义成AFTER。
如果想获得执行时间,必须 把Where定义成AFTER。

注意这里,一定不要像下面这样大范围的匹配,否则这性能是神仙也没法救了:

@OnMethod(clazz = "/javax\\.swing\\..*/", method = "/.*/", location = @Location(value = Kind.CALL, clazz = "/.*/", method = "/.*/"))

下例监控代码是否到达了Socket类的第363行。

@OnMethod(clazz = "java.net.ServerSocket", location = @Location(value = Kind.LINE, line = 363))

public static void onBind4() {

   println("socket bind reach line:363");

}

line还可以为-1,然后每行都会打印出来,加参数int line 获得的当前行数。此时会显示函数里完整的执行路径,但肯定又非常慢。

4. 打印this,参数 与 返回值

4.1 定义注入

import com.sun.btrace.AnyType;

@OnMethod(clazz = "java.io.File", method = "createTempFile", location = @Location(value = Kind.RETURN))

public static void o(@Self Object self, String prefix, String suffix, @Return AnyType result)

如果想打印它们,首先按顺序定义用@Self 注释的this, 完整的参数列表,以及用@Return 注释的返回值。

需要打印哪个就定义哪个,不需要的就不要定义。但定义一定要按顺序,比如参数列表不能跑到返回值的后面。

Self:

如果是静态函数, self为空。

前面提到,如果上述使用了非JDK的类,命令行里要指定classpath。不过,如前所述,因为BTrace里不允许调用类的方法,所以定义具体类很多时候也没意思,所以self定义为Object就够了。

参数:

参数数列表要么不要定义,要定义就要定义完整,否则BTrace无法处理不同参数的同名函数。

如果有些参数你实在不想引入非JDK类,又不会造成同名函数不可区分,可以用AnyType来定义(不能用Object)。

如果拦截点用正则表达式中匹配了多个函数,函数之间的参数个数不一样,你又还是想把参数打印出来时,可以用AnyType[] args来定义。

但不知道是不是当前版本的bug,AnyType[] args 不能和 location=Kind.RETURN 同用,否则会进入一种奇怪的静默状态,只要有一个函数定义错了,整个Btrace就什么都打印不出来。

结果:

同理,结果也可以用AnyType来定义,特别是用正则表达式匹配多个函数的时候,连void都可以表示。

 

4.2 打印

再次强调,为了保证性能不受影响,Btrace不允许调用任何实例方法。
比如不能调用getter方法(怕在getter里有复杂的计算),只会通过直接反射来读取属性名。
又比如,除了JDK类,其他类toString时只会打印其类名+System.IdentityHashCode。
println, printArray,都按上面的规律进行,所以只能打打基本类型。

如果想打印一个Object的属性,用printFields()来反射。

如果只想反射某个属性,参照下面打印Port属性的写法。从性能考虑,应把field用静态变量缓存起来。

注意JDK类与非JDK类的区别:

import java.lang.reflect.Field;

//JDK的类这样写就行

private static Field fdFiled = field("java.io,FileInputStream", "fd");


//非JDK的类,要给出ClassLoader,否则ClassNotFound

private static Field portField = field(classForName("com.vip.demo.MyObject", contextClassLoader()), "port");


public static void onChannelRead(@Self Object self) {

    println("port:" + getInt(portField, self));

}


 

4.3.TLS,拦截函数间的通信机制

如果要多个拦截函数之间要通信,可以使用@TLS定义 ThreadLocal的变量来共享

@TLS

private static int port = -1;


@OnMethod(clazz = "java.net.ServerSocket", method = "<init>")

public static void onServerSocket(int p){

    port = p;

}

@OnMethod(clazz = "java.net.ServerSocket", method = "bind")

public static void onBind(){

  println("server socket at " + port);

}

 

5. 典型场景

5.1 打印慢调用

下例打印所有用时超过1毫秒的filter。

@OnMethod(clazz = "+com.vip.demo.Filter", method = "doFilter", location = @Location(Kind.RETURN))

public static void onDoFilter2(@ProbeClassName String pcn,  @Duration long duration) {

    if (duration > 1000000) {

        println(pcn + ",duration:" + (duration / 100000));

    }

}

最好能抽取了打印耗时的函数,减少代码重复度。

定位到某一个Filter慢了之后,可以直接用Location(Kind.CALL),进一步找出它里面的哪一步慢了。

 

5.2 谁调用了这个函数

比如,谁调用了System.gc() ?

@OnMethod(clazz = "java.lang.System", method = "gc")

public static void onSystemGC() {

    println("entered System.gc()");

    jstack();

}

 

5.3 捕捉异常,或进入了某个特定代码行时,this对象及参数的值

按之前的提示,自己组合一下即可。

 

5.4 打印函数的调用/慢调用的统计信息

如果你已经看到了这里,那基本也不用我再啰嗦了,自己看Samples的Histogram.java, HistoOnEvent.java

可以用AtomicInteger构造计数器,然后定时(@OnTimer),或根据事件(@OnEvent)输出结果(ctrl+c后选择发送事件)。

 

发现自己还是喜欢这些不漂亮的照片。

String.intern() 祛魅

| Filed under 技术

String.intern(),写应用的同学不知道也没什么损失。但知道的同学,如果只觉得它很省内存,用==比较字符串很酷,或者期待JDK会做什么神奇的事情,然后恨不得所有地方都用上String.intern(),那很可能会误用,所以把上周在群里的讨论整理成此文。

在社会科学中,祛魅(Disenchantment)是指在现代社会中消去神秘主义(魅惑力)的表面并把文化合理化。这里乱用一下。

 

1. String.itern()的基本原理

详细可看占小狼同学的《浅谈Java String内幕(2)》

String.intern()是一个Native方法,底层调用C++的 StringTable::intern 方法,源码注释:当调用 intern 方法时,如果常量池中已经该字符串,则返回池中的字符串;否则将此字符串添加到常量池中,并返回字符串的引用。

所以明面上,它有两大好处,一是重复的字符串,会用同一个引用代替;二是字符串比较,不再需要逐个字符的equals()比较,而用==对比引用是否相同即可。

 

2. 省内存效果只对长期存在的字符串有效

String.intern()没有神奇的地方,只在字符串生成后,再去常量池里查找引用。所以字符串最初生成时所花的内存,是省不掉的。

String s = new String(bytes, “UTF-8”).intern();
String s = String.valueOf(i).intern();

只有大量对象放在长期存在的集合里,里面是大量重复的字符串,或者对象的属性是重复的字符串时,省内存的效果才显现出来。短生命周期的字符串,GC要干的活是一样的。

 

3. 执行路径上多次的==,才能抵消常量池HasHMap查找的代价

==当然比equals()快得多,但常量池其实是个HashMap,依然没有神奇的地方,依然要执行HashMap的get操作,所以,一次hashCode() 和至少一次的equals()已经预付了,如果hash冲突,那equals()次数更多。

 

4. 真的对性能影响甚微吗?

在我的服务化框架测试里,把几个Header字段intern了,性能立马从七万五调到七万一 QPS,原来从七万一升到七万五 ,曾做过多少效果甚微的优化加上一次Netty使用的优化而成,现在它掉下来倒是飞快。

PS. 七万五 20%CPU这个数字,这两周的博客里都没升过了: (

 

5. 小陷阱

来自R大的提醒, s.intern()是无效的,因为String是不变对象, String s1 = s.intern()后,这个s1才是个引用。

Java的常量池也是不省心的,要注意JDK版本,占小狼同学的《浅谈Java String内幕(2)》

 

PS. 唯品会广州的基础架构部现在只剩最后一个架构师的名额了,欲投简历请趁早, calvinxiao@vipshop.com

 

 

by calvin | tags : | 6